Alexandr Antonov
Alexandr Antonov

Reputation: 35

Cats Free Monad based algebras composition

Suppose I've got the following algebra for working with file system:

sealed trait Fs[A]
case class Ls(path: String) extends Fs[Seq[String]]
case class Cp(from: String, to: String) extends Fs[Unit]

def ls(path: String) = Free.liftF(Ls(path))
def cp(from: String, to: String) = Free.liftF(Cp(from, to))

And the following interpreter for the algebra:

def fsInterpreter = new (Fs ~> IO) {
  def apply[A](fa: Fs[A]) = fa match {
    case Ls(path) => IO(Seq(path))
    case Cp(from, to) => IO(())
  }
}

Now suppose I want to build another algebra that uses the first one. E.g.:

sealed trait PathOps[A]
case class SourcePath(template: String) extends PathOps[String]

def sourcePath(template: String) = Free.liftF(SourcePath(template))

The next thing I want to write an interpreter for PathOps ~> IO which would do something like this:

for {
  paths <- ls(template)
} yield paths.head

In other words my interpreter for PathOps should call into Fs algebra.

How do I do that?

Upvotes: 3

Views: 238

Answers (1)

Andrey Tyukin
Andrey Tyukin

Reputation: 44967

I assume that you want to write two interpreters PathOps ~> Free[Fs, ?] and Fs ~> IO, and then to compose them into a single interpreter PathOps ~> IO.

A compilable example follows. Here are all the imports that I used for this example:

import cats.~>
import cats.free.Free
import cats.free.Free.liftF

Here is a mock-implementation of IO and your algebras:

// just for this example
type IO[X] = X 
object IO {
  def apply[A](a: A): IO[A] = a
}

sealed trait Fs[A]
case class Ls(path: String) extends Fs[Seq[String]]
case class Cp(from: String, to: String) extends Fs[Unit]
type FreeFs[A] = Free[Fs, A]

def ls(path: String) = Free.liftF(Ls(path))
def cp(from: String, to: String) = Free.liftF(Cp(from, to))

This is the interpreter Fs ~> IO copied from your code:

def fsToIoInterpreter = new (Fs ~> IO) {
  def apply[A](fa: Fs[A]) = fa match {
    case Ls(path) => IO(Seq(path))
    case Cp(from, to) => IO(())
  }
}

sealed trait PathOps[A]
case class SourcePath(template: String) extends PathOps[String]

def sourcePath(template: String) = Free.liftF(SourcePath(template))

This is your for-comprehension converted into a PathOps ~> Free[Fs, ?]-interpreter:

val pathToFsInterpreter = new (PathOps ~> FreeFs) {
  def apply[A](p: PathOps[A]): FreeFs[A] = p match {
    case SourcePath(template) => {
      for {
        paths <- ls(template)
      } yield paths.head
    }
  }
}

Now you can lift the Fs ~> IO into an Free[Fs, ?] ~> IO using Free.foldMap, and compose it with the PathOps ~> Free[Fs, ?]-interpreter using andThen:

val pathToIo: PathOps ~> IO = 
  pathToFsInterpreter andThen 
  Free.foldMap(fsToIoInterpreter)

This gives you an interpreter from PathOps ~> IO that consists of two separate layers that can be tested separately.

Upvotes: 2

Related Questions