List of classes implementing a certain typeclass

后端 未结 4 951
陌清茗
陌清茗 2021-02-15 11:02

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:29

    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)
    

提交回复
热议问题