When using type classes, how to handle an object in different ways?

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

Now I have an implicit one that allows me to consider sets of pairs of type A as a graph with vertices of type A (unable to express unconnected vertices ...). I can use implicit by importing the next area of ​​the object.

 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 display the contents of the vertices, thereby having the ability to do the following:

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

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

+7
source share
3 answers

Draw a possible solution:

Put the mapping operation in an auxiliary characteristic

say GraphOps (it could be Graph , but maybe the cartographic signature would be too complicated)

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

Easy to get GraphOps :

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

In this case, the call will be

 Graph(set).map(f) 

apply may be implicit, but I'm not sure I want to do this (and if I did, I’m not sure that it will find the map properly).

Option. Have a 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) } 

A good point is that the vertex type V is available in GraphOps

Display operation definition

Your signature is complex: Set [(A, A)] returns Set [(B, B)], but other graph implementations return something completely different. This is similar to what is done in the collection library.

We can introduce the sign CanMapGraph [From, Elem, To], akin to CanBuildFrom

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

(perhaps you would change this to more basic operations than on the map so that it can be used for different operations, as is done with CanBuildFrom )

Then the mapping will 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)) 

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

With an alternative to GraphOps , where we get an early vertex type, A not a Map parameter, but a GraphOps , so it is known from the very beginning and does not have to be explicit in f . You do this so you can pass the graph to the map method in CanMapGraph .

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

 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 } 
+3
source
 val x: Set[(A, A)] = ... (x: Graph[_, _]).map(...) 

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

As you point out, this is 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 let f be V => V , not V => V1 . What for? Imagine that you have implicit g1: Graph[SomeType, Int] , but not implicit g2: Graph[SomeType, String] . What then could return Graph.map(_: SomeType)((_: Int).toString) ? This problem can be avoided by requiring G be a parameterized 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]) = ... } 
+1
source

If you use type classes, you can do something like this:

 implicitly[TypeClass].map(...) 

If you use view boundaries then Alexey's answer is correct:

 (...: ViewBound).map(...) 
0
source

All Articles