List of classes implementing a certain typeclass

后端 未结 4 1552
深忆病人
深忆病人 2021-02-15 10:47

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 int         


        
相关标签:
4条回答
  • 2021-02-15 11:16

    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.

    0 讨论(0)
  • 2021-02-15 11:26

    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)
    
    0 讨论(0)
  • 2021-02-15 11:26

    Disclamer: I've made this answer to provide a solution to the concrete development problem, and not the theoretical problem of using typeclass


    I would do it this way:

    trait Showable{ def show(): String }
    
    implicit class IntCanShow(int: Int) extends Showable {
      def show(): String = s"int $int"
    }
    
    implicit class StringCanShow(str: String) extends Showable {
      def show(): String = str
    }
    
    val l: List[Showable] = List(1,"asd")
    

    Note that I changed the meaning of the trait Show, into Showable, such that the implementing classes are used as wrapper. We can thus simply require that we want Showable instances (and because those classes are implicit, input of the List are automatically wrapped)

    0 讨论(0)
  • 2021-02-15 11:31

    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.

    0 讨论(0)
提交回复
热议问题