I would like to define a List
of elements implementing a common type class. E.g.
trait Show[A] {
def show(a: A): String
}
implicit val intCanShow: Show[Int] = new Show[Int] {
def show(int: Int): String = s"int $int"
}
implicit val stringCanShow: Show[String] = new Show[String] {
def show(str: String): String = str
}
The problem is, how to define a list = List(1, "abc")
such that it is guaranteed that a Show
instance for these values is in scope? I would then like to map this list over show like list map {_.show}
.
I will first sketch a solution, and then explain why the naive approach with List[Any](1, "abc")
cannot work.
What you can do
Define a wrapper class that can hold instances of type A
together with instances of Show[A]
:
case class Showable[A](a: A, showInst: Show[A]) {
def show: String = showInst.show(a)
}
Define your list as List[Showable[_]]
:
var showableList: List[Showable[_]] = Nil
Maybe define a separate method to fill this list (consider packing the list itself and the builder-method in a class):
def addShowable[A: Show](a: A): Unit = {
showableList ::= Showable[A](a, implicitly[Show[A]])
}
Alternatively, you can carefully add a (very tightly scoped) implicit conversion:
implicit def asShowable[A](a: A)(implicit s: Show[A]): Showable[A] =
Showable(a, s)
and then costruct your list as follows (note the explicit type ascription):
val showableList = List[Showable[_]](1, "abc")
Now you can go through the list and call show
:
showableList.map(_.show)
to obtain a list of String
.
What you cannot do
You cannot simply define
val list: List[Any] = List(1, "abc", <showable3>, ..., <showableN>)
and then expect to be able to call show
, because in order to call Show.show
, you need actual Show
instances. These things are not some type-hints that can be erased at runtime, they are actual objects, and they must be supplied by the compiler. Once you have created a List[Any]
, all is lost, because all the types are merged into an unexpressive upper bound Any
, and the compiler has no way to inject all the necessary implicits Show[T_1]
,..., Show[T_N]
. The argument is very similar to the third section "Dealing with implicits when defining interpreter for the Free monad" of this lengthy answer of mine.
An alternative way of handling this would be to use the shapeless library. I would really reccommend this book which explains shapeless in a clear and concise manner.
Shapeless provides two things that I think will help you in this case:
First import the required libraries (shapeless):
import shapeless.{HNil, Poly1, ::}
Create a heterogeneous list of whatever types you require. Note the type annotation is only there for clarity.
val data : Int :: String :: HNil = 1 :: "hello" :: HNil
Create a polymorphic function defining an implicit value for every type you require.
object Show extends Poly1 {
implicit def atT[T: Show] = at[T] (implicitly[Show[T]].show)
}
Shapeless provides an extension method for map on a HList to enable applying the show function to every element in the list
val result : String :: String :: HNil = data.map(Show)
Edited: thanks to @dk14 for the suggested improvement to the definition of the Show polymorphic function.
The core problem here is that you want to create a heterogenous list, something like List[Int, String]
instead of List[Any]
. This means you need a different structure that would preserve Int
and String
types, but still would be "mappable" like List. The one structure in scala-library that can contain heterogenous types is Tuple:
val tuple = (1, "abc")
val result = List(implicitly[Show[Int]].show(tuple._1), implicitly[Show[Int]].show(tuple._2))
However, scala-library can't map
over tuples - you might want some syntax sugar for better readability.
So the obvious solution is HList from Shapeless: Int :: String :: HNil
(or you can use tuple ops and stay with (Int, String)
)
import shapeless._
import poly._
//show is a polymorphic function
//think of it as `T => String` or even `(Show[T], T) => String`
object show extends Poly1 {
implicit def atT[T: Show] = at[T](implicitly[Show[T]].show)
}
@ (1 :: "aaaa" :: HNil) map show
res8: String :: String :: HNil = "int 1" :: "aaaa" :: HNil
Or you could use at[Int]
/at[String]
instead of type-classes, like in @Steve Robinson's answer.
P.S. The lib could be found here. They also provide one-liner to get Ammonite REPL with shapeless integrated, so you could try my example out using:
curl -s https://raw.githubusercontent.com/milessabin/shapeless/master/scripts/try-shapeless.sh | bash
Notes:
Practically Shapeless solution requires as same amount of maintenance as Tuple-based one. This is because you have to keep track of your Int
and String
types anyways - you can never forget about those (unlike in homogenous List[T]
case). All Shapeless does for you is nicer syntax and sometimes better type inference.
If you go with tuples - you can improve readability by using implicit class
instead of Haskell-like style, or if you still want Haskell-like, there is a Simulacrum macro for better type-class syntax.
Given that other scala-library-only alternatives just capture type class instances inside some regular class, you could be better off with a regular OOP wrapper class:
trait Showable[T]{def value: T; def show: String}
class IntShow(val value: Int) extends Showable[Int]{..}
class StringShow(val value: String) extends Showable[String] {..}
val showables: List[Showable[_]] = List(new Showable(5), new Showable("aaa"))
showables.map(_.show)
Looks cleaner and more readable to me :)
If you like to rewrite dynamic dispatching in FP-style:
sealed trait Showable
final case class ShowableInt(i: Int) extends Showable
final case class ShowableString(s: String) extends Showable
implicit class ShowableDispatch(s: Showable){
def show = s match{ //use `-Xfatal-warnings` scalac option or http://www.wartremover.org/ to guarantee totality of this function
case ShowableInt(i) => ...
case ShowableString(s) => ...
}
}
List(ShowableInt(5), ShowableString("aaa")).map(_.show)
If you really want static dispatching (or ad-hoc polymorphism), given that other solutions introduce Showable[_]
which is practically Showable[Any]
:
case class Showable[T](v: T, show: String)
def showable(i: Int) = Showable(i, s"int $i")
def showable(s: String) = Showable(i, s)
List(showable(5), showable("aaa")).map(_.show)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With