wdb wdb - 3 months ago 12
Scala Question

How can I make a scala method parameter type that is a collection of multiple types that can be converted to a given type?

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!

Answer

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 Widgets

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.

Comments