Reputation: 343
I want to create a scala method parameter type that is a collection of multiple types which can all be converted to a common given type, but I have been unable to do that using implicit conversion, type classes, context bounds, and view bounds.
I have created a trait and class classes extending the trait, and I want to write a method that allows other developers to pass in a collection of instances that my code will process. However, I don't want the caller of my method to create a collection of instances. Instead, I want the caller to pass in a collection of tuples, where the tuples must be from a defined set of tuples.
The trait and case classes look like this:
sealed trait Widget {
type W
def identifier: W
def number: Int
}
case class CWidget(identifier: Char, number: Int) extends Widget {type W = Char}
case class SWidget(identifier: String, number: Int) extends Widget {type W = String}
case class CSWidget(identifier: (Char, String), number: Int) extends Widget {type W = (Char, String)}
and the method would look like this:
def getWidgets[W <% Widget](s: Seq[W]): Unit = ...
For now, the allowed tuples would have these types: (Char, Int)
, (String, Int)
, and (Char, String, Int)
. Eventually, I would like to be able to allow tuples that mix up the order like (Int, Char)
, but that's another story.
I created these implicit methods and brought them into scope with an import
:
implicit def fromTuple(t: (Char, Int)) = ((c: Char, n: Int) => CWidget(c, n)) tupled t
implicit def fromTuple(t: (String, Int)) = ((s: String, n:Int) => SWidget(s, n)) tupled t
implicit def fromTuple(t: (Char, String, Int)) = ((c: Char, s: String, n: Int) => CSWidget((c, s), n)) tupled t
I tried this from the REPL:
getWidgets(Seq(('a',1),("string",2),('c',"string",3)))
and received this error:
<console>:26: error: No implicit view available from Product with Serializable => Widget.
Thanks in advance!
EDITED:
Here is the result of implementing the answer (Thanks, @gregghz !):
sealed trait Widget {
type W
def identifier: W
def number: Int
}
case class CWidget(identifier: Char, number: Int) extends Widget {type W = Char}
case class SWidget(identifier: String, number: Int) extends Widget {type W = String}
case class CSWidget(identifier: (Char, String), number: Int) extends Widget {type W = (Char, String)}
implicit def fromTupleCI(t: (Char, Int)): Widget = {
val (c, n) = t
CWidget(c, n)
}
implicit def fromTupleSI(t: (String, Int)): Widget = {
val (s, n) = t
SWidget(s, n)
}
implicit def fromTupleCSI(t: (Char, String, Int)): Widget = {
val (c, s, n) = t
CSWidget((c, s), n)
}
def getWidgets(s: Widget*): Unit = {
s.foreach { w => println(w) }
}
getWidgets(('a',1), ('b',2), ('c', "abc", 3), ("abcd", 4)
Here are the results:
CWidget(a,1)
CWidget(b,2)
CSWidget((c,abc),3)
SWidget(abcd,4)
This is good, since I didn't want to force the caller to create a Seq for the getWidgets
method.
Most importantly, it checks the types of the arguments at compile time (Thanks, Static Typing!). I could have made the getWidgets
arguments Any
and used pattern matching, but that would have checked the arguments are run-time.
Upvotes: 1
Views: 356
Reputation: 3965
There are few problems here.
Your implicit conversion functions are converting from Tuple2[Char, Int]
to Tuple2[Char, Int] => Widget
. You want them to convert directly to a widget.
They should look more like this:
implicit def fromTuple(t: (Char, Int)): Widget = {
val (c, n) = t
CWidget(c, n)
}
implicit def fromTuple(t: (String, Int)): Widget = {
val (s, n) = t
SWidget(s, n)
}
implicit def fromTuple(t: (Char, String, Int)): Widget = {
val (c, s, n) = t
CSWidget((c, s), n)
}
However, this won't work as is. def fromTuple(t: (Char, Int))
and def fromTuple(t: (String, Int))
are the same type after type erasure occurs. This means the runtime can't distinguish between these two functions and it doesn't know which one to call. It turns out this is easy to fix. Just change the names of the functions to be unique:
implicit def fromTupleCI(t: (Char, Int)) = ...
implicit def fromTupleSI(t: (String, Int)) = ...
implicit def fromTupleCSI(t: (Char, String, Int) = ...
Next, since your call to getWidgets
takes a Seq
, the type of the Seq
is first inferred. Since the various tuples have a common parent of Product
your Seq
is roughly inferred to be a Seq[Product]
. There is no implicit conversion from Seq[Product]
to a Seq[Widget]
. There are two solutions to this.
You can explicitly specify the type parameter to Seq
:
getWidgets(Seq[Widget](('a',1),("string",2),('c',"string",3)))
That will trigger the implicit conversion of the tuples to Widget
s
You can also change the signature of getWidgets
to take a varargs of widgets.
def getWidgets(s: Widget*): Unit = ...
getWidgets(('a',1), ("string",2), ('c',"string",3))
Again, this triggers implicit conversion where you want it to happen.
As a side note, view bounds (<%
) is deprecated. In this case, you can simply replace <%
with <:
and get the same results.
Upvotes: 1