scalascalazscala-catsscala-3

How to create a Functor for an ADT used in a cats Free Monad


I am writing a DSL using case classes with the help of the cats.free.Free Monad library. The DSL is to be interpreted by Actors receiving the message, so each actor has to first unwrap a command using Free.resume.

This worked out nicely six years ago using scalaz where we also used the resume function too, but the Functor for the free monad was easy to create as we used case classes that came with an extra function argument that could be mapped over such as k below.

case class GetResource[Rdf <: RDF, A](
   uri: Rdf#URI,
   k: NamedResource[Rdf] => A) extends LDPCommand[Rdf, A]

But the current examples for cats.scala.Free on the cats Free Monad web page don't come with such an argument. And indeed those work nicely when using the interpretation of Free Monads via a natural transformation. I tried this out with a super simple DSL with only one case class

sealed trait LDPCmd[A]:
    def url: Uri

case class Get[T](url: Uri) extends LDPCmd[Response[T]]

For which I can then write a simple Script which works as expected in the tests using the natural transformation interpretation.

But with the Actors based interpretation, I now need to unwrap each command in the free monad which gets sent around to different actors using the resume function of Free. This requires a Functor. But it is not clear to me where the functor can get a hold. Ie, what do I put in the ??? position here

given CmdFunctor: cats.Functor[LDPCmd] with 
    def map[A, B](fa: LDPCmd[A])(f: A => B): LDPCmd[B] = fa match 
        case g: Get => ???

Solution

  • The answer is it seems to return back to adding the extra function to the case class. On the cats chat channel Rob Norris wrote:

    The classical encoding of Free[F, *] is a monad only if F is a functor, so you needed the extra Blah => A member for each case. Then we figured out that you could use Free[Coyoneda[F, *], *] and get that machinery for free. Then we figured out how to just bake it into Free, so you don’t need to do any of that most of the time. But if you need to walk through your computation step by step you do need one of those encodings because F still needs to be a Functor in that case.

    The code he pointed to is SimMessageSocket.scala.

    sealed trait Step[+A]
      case class Send[A](m: BackendMessage, k: Unit => A) extends Step[A]
      case class Expect[A](h: PartialFunction[FrontendMessage, A]) extends Step[A]
      object Step {
        implicit val FunctorStep: Functor[Step] =
          new Functor[Step] {
            def map[A,B](fa: Step[A])(f: A => B): Step[B] =
              fa match {
                case Send(m, k) => Send(m, k andThen f)
                case Expect(h)    => Expect(h andThen f)
              }
          }
      }