When using type classes, how to deal with object in different ways?

Suppose I have a type class Graph[G,V] which states that an object of type G is also a graph with vertices of type V .

Now I have an implicit that lets me treat sets of pairs of type A as a graph with vertices of type A (not being able to express unconnected vertices...). I can use the implicit by importing the following object's scope.

object TupleSetGraph{
  implicit def ts2graph[A]: Graph[Set[(A,A)],A] = new Graph[Set[(A,A)],A] {
    def nodes(g: Set[(A, A)]): Set[A] = g flatMap (t => Set(t._1,t._2))
    def adjacent(g: Set[(A, A)], n1: A, n2: A): Boolean = g.contains((n1,n2)) || g.contains((n2,n1))
  }
}

Suppose I also want to be able to map the content of the vertices, thus being able to do the following:

(_: Set[(A,A)]).map((_: A => B)): Set[(B,B)]

But there is already a map defined on Set . How to deal with the problem that the same data structure can be seen as the same thing (something having a map function) in different ways?


Sketching a possible solution :

Put the map operation in an auxiliary trait

say GraphOps (that could be Graph itself, but map signature will probably be too complex for that)

case class GraphOps[G](data: G) { def map...}

Making it easy to get the GraphOps :

object Graph {
   def apply[G](data: G) = GraphOps(data)
}

With that, the call will be

Graph(set).map(f) 

apply could be made implicit, but I'm not sure I want to do that (and if I did, I'm not sure it would find map properly).

Variant. Have the graph in GraphOps

we can also do

case class GraphOps[G,V](data: G, graph: Graph[G,V])

and

object Graph {
   def apply[G,V](data: G)(implicit graph: Graph[G,V]) = GraphOps(data, graph)
}

The good point of that is that vertex type V is available in GraphOps

Defining the map operation

The signature you want is complex, with Set[(A,A)] returning a Set[(B,B)], but other graph implementations returning something completely different. This is similar to what is done in the collection library.

We may introduce a trait CanMapGraph[From, Elem, To], akin to CanBuildFrom

trait CanMapGrap[FromGraph, FromElem, ToGraph, ToElem] {
  def map(data: FromGraph, f: FromElem => ToElem): ToGraph
}

(probably you would change this to have more elementary operations than map, so that it may be used for different operations, as done with CanBuildFrom )

Then map would be

case class GraphOps[G](data: G) {
  def map[A,B](f: A, B)(implicit ev: CanMapFrom[G, A, B, G2]) : G2 =
    ev.map(data, f)
}

You can define

implicit def mapPairSetToPairSet[A, B] = 
  new CanMapGraph[Set[(A,A)], A, Set[(B,B)], B] {
    def map(set: Set[(A,A)], f: A => B) = set.map{case (x, y) => (f(x), f(y))}
  } 

And then you do

val theGraph = Set("A" -> "B", "BB" -> "A", "B" -> "C", "C" -> "A")
Graph(theGraph).map(s: String -> s(0).toLower)
res1: Set[(Char, Char)] = Set((a,b), (b,a), (b,c), (c,a))

A problem with that is that the type of the vertices is not known in the first argument list, the one for f, so we have to be explicit with s: String.

With the alternative GraphOps , where we get the vertex type early, A is not a parameter of Map, but of GraphOps , so it is known from the start and does not need to be explicit in f . It you do it that way, you may want to pass the graph to method map in CanMapGraph .

With the first solution, it is still easy to give the graph to the CanMapGraph .

implicit def anyGraphToSet[G,V,W](implicit graph: Graph[G,V]) 
  = new CanMapFrom[G, V, Set[(W,W)], W] {
    def map(data: G, f: V => W) = 
      (for {
         from <- graph.nodes(data)
         to <- graph.nodes(data)) 
         if graph.adjacent(data, from, to) }
       yield (from, to)).toSet
  }

val x: Set[(A, A)] = ...
(x: Graph[_, _]).map(...)

seems to be the best you can do if you want the names to be the same.

As you point out, that's not what you want. This should work better:

object Graph {
  def map[G, V](graph: G)(f: V => V)(implicit instance: Graph[G, V]) = ...
}

val x: Set[(A, A)] = ...
Graph.map(x)(f) 
// but note that the type of argument of f will often need to be explicit, because
// type inference only goes from left to right, and implicit arguments come last

Note that you can only let f to be V => V and not V => V1 . Why? Imagine that you have implicit g1: Graph[SomeType, Int] , but not implicit g2: Graph[SomeType, String] . What could Graph.map(_: SomeType)((_: Int).toString) return then? This problem can be avoided by requiring G to be a parametrized type:

trait Graph[G[_]] {
  def nodes[A](g: G[A]): Set[A]
  def adjacent[A](g: G[A], n1: A, n2: A): Boolean
}

object TupleSetGraph{
  type SetOfPairs[A] = Set[(A,A)]
  implicit def ts2graph: Graph[SetOfPairs] = new Graph[SetOfPairs] {
    def nodes[A](g: Set[(A, A)]): Set[A] = g flatMap (t => Set(t._1,t._2))
    def adjacent[A](g: Set[(A, A)], n1: A, n2: A): Boolean = g.contains((n1,n2)) || g.contains((n2,n1))
  }
}

then you have

object Graph {
  def map[G[_], V, V1](graph: G[V])(f: V => V1)(implicit instance: Graph[G]) = ...
}

If you are using type classes, then you can do something like this:

implicitly[TypeClass].map(...)

If you are using view bounds, then Alexey's answer is correct:

(...: ViewBound).map(...)
链接地址: http://www.djcxy.com/p/55754.html

上一篇: 统计电话的数量

下一篇: 在使用类型类时,如何以不同的方式处理对象?