Search code examples
scalagenericshigher-kinded-types

Generic container type (higher kind) for "map" in scala


I am trying to define a case class that is parameterized with a container type (higher kind). This container type can be anything as long as it has a map method defined.

I would like to achieve this result suggested by the following code:

import scala.language.higherKinds

case class Test[A, C[A]](init: A, trans: Map[A,C[A]]) {
  def convert[B](conv: A => B): Test[B, C[B]] = {
    val _init = conv(init)
    val _trans = trans map {case (k,v) => (conv(k) -> (v map {x => conv(x)})}
    Test(_init, _trans)
  }
}

the problem lies in the v map {x => conv(x)} part of the code. Since no bound was defined for C[A] it, obviously, does not compile.

The thing is that this container type can be an Id (Scalaz style, but with map instead of |>), and Option, or a collection (Seq, Set, List, etc.)

Is there any way to tell the scala compiler that the container type will must have the map method?


Solution

  • The best way to achieve what you want is by using the Functor type class. For example here is an example using Cats.
    (You could do the same using Scalaz).

    import scala.language.higherKinds
    
    import cats.Functor
    import cats.syntax.functor.toFunctorOps
    
    final case class Test[A, C[_]](init: A, trans: Map[A,C[A]])(implicit CFunctor: Functor[C]) {
      def convert[B](conv: A => B): Test[B, C] = {
        val _init = conv(init)
        val _trans = trans map { case (k,v) => conv(k) -> v.map(x => conv(x)) }
        Test(_init, _trans)
      }
    }
    

    However, if for some reason you can't or don't want to use Cats/Scalaz, you could try Structural Types.

    import scala.language.higherKinds
    import scala.language.reflectiveCalls
    
    final case class Test[A, C[A] <: { def map[B](f: A => B): C[B]}](init: A, trans: Map[A,C[A]]){
      def convert[B](conv: A => B): Test[B, C] = {
        val _init = conv(init)
        val _trans = trans map { case (k,v) => conv(k) -> v.map(x => conv(x)) }
        Test(_init, _trans)
      }
    }
    

    Nevertheless, note that the last one will work for Option, but will fail for List, simple because the map method in List receives an implicit CanBuildFrom, and for that reason it isn't equals to the one you want.