Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

List of classes implementing a certain typeclass

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}.

like image 898
Manuel Schmidt Avatar asked Mar 12 '18 16:03

Manuel Schmidt


3 Answers

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.

like image 198
Andrey Tyukin Avatar answered Nov 17 '22 12:11

Andrey Tyukin


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:

  1. Heterogeneous lists (HList)
  2. Polymorphic functions to enable the HList mapping operation.

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.

like image 32
Steve Robinson-Burns Avatar answered Nov 17 '22 10:11

Steve Robinson-Burns


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:

  1. 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.

  2. 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)
like image 27
dk14 Avatar answered Nov 17 '22 10:11

dk14