Reputation: 35
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
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