Search code examples
scalafunctional-programmingfunctorscala-cats

Scala: How to use Functors for multi type parameter trait


I have and ADT which is basically a wrapper over Function1:

case class Abstract[M[_], A, B](f:M[A] => M[B]) {
    def fn: M[A] => M[B] = { case x: M[A] => f(x) }
}

I want to map over these, so I defined a Functor like so:

trait AbstractAPI[E] {
    type AbsO[T] = Abstract[List, E, T]
    // type AbsO[T] = Abstract[List, _, T] => does not work (?)

    implicit val abstractO: Functor[AbsO] = new Functor[AbsO] {
        def map[A, B](fa: AbsO[A])(f: A => B): AbsO[B] = {
            new Abstract(fa.fn andThen { x: List[A] => x.map{ y => f(y) } })
        }
    }
}

Now, to actually map over an Abstract, I'd need AbstractAPI[Int], like

case object IntAbstractAPI extends AbstractAPI[Int]

object A {
    import IntAbstractAPI._

    val f:List[Int] => List[String] = { case x: List[Int] => x.map{ _.toString.toLowerCase } }
    val hey = (new Abstract(f)).map{ x => x.toInt }
}

or

object A extends AbstractAPI[Int] {

    val f:List[Int] => List[String] = { case x: List[Int] => x.map{ _.toString.toLowerCase } }

    // FINALLY!
    val res = (new Abstract(f)).map{ x => x.toInt }.map{ _.toFloat + 10f }
    // Abstract[List, Int, Float] = Abstract(<function1>)
}

However, in this pattern, I'd have to define case objects for every possible E. Here are my questions:

  1. Is this the correct way to use Functors?
  2. How can I automate the creation of the case objects for every possible E (or make the compiler infer it?)

Edit 1: Further clarification: The above implementation works, but this one does not:

object A extends AbstractAPI {

    val f:List[Int] => List[String] = { case x: List[Int] => x.map{ _.toString.toLowerCase } }

    val res = (new Abstract(f)).map{ x => x.toInt }.map{ _.toFloat + 10f }
    // Abstract[List, Int, Float] = Abstract(<function1>)
}

gives compilation error:

value map is not a member of Abstract[List,Int,String]

I assume this is because the compiler is not able to derive a functor for Abstract[List,Int,String]?


Solution

  • You can derive a functor for type parameters that you don't care about.

    import cats.Functor
    import cats.syntax.functor._
    

    And I'll rename second type parameter on Abstract to X, it'll help

    case class Abstract[M[_], X, A](f: M[X] => M[A]) // forget the fn bit for now
    

    You can create typeclass instances not only with a val, but also with a def. It is allowed to have type parameters and also take other implicit (but only implicit) parameters.

    type Abs1[X] = ({ type L[A] = Abstract[List, X, A] })
    
    /*implicit*/ def abstract1[X]: Functor[Abs1[X]#L] = new Functor[Abs1[X]#L] {
    override def map[A, B](fa: Abstract[List, X, A])(f: A => B): Abstract[List, X, B] =
          Abstract(mx => fa.f(mx).map(f))
    }
    

    If map is all you need from a List, you can generalize further for any M[_] that has a Functor instance. Also placing it into a companion object of Abstract enables it to be found without additional imports / inheritance / etc.

    object Abstract {
      // Abstract.MX[M, X]#L can be replaced with Abstract[M, X, ?] if you use kind-projector
      type MX[M[_], X] = ({ type L[A] = Abstract[M, X, A] })
    
      implicit def genericFunctor[M[_]: Functor, X] = new Functor[MX[M, X]#L] {
        override def map[A, B](fa: Abstract[M, X, A])(f: A => B): Abstract[M, X, B] =
          Abstract(mx => fa.f(mx).map(f)) // the implementation is the same
      }
    }
    

    And it works, if you import instances for whatever your M[_] is

    assert {
      import cats.instances.list._ // get Functor[List]
    
      // map is automatically picked up from Functor[Abstract[List, Int, ?]]
      Abstract(identity[List[Int]])
        .map(Vector.range(0, _))
        .map(_.mkString(""))
        .f(List(1, 2, 3)) == List("0", "01", "012")
    }
    
    assert {
      import cats.instances.option._
    
      Abstract(identity[Option[Int]])
        .map(_ min 42)
        .map(i => Range(i, i + 3))
        .f(Some(11)) == Some(Range(11, 14))
    }
    

    You can try the code there