Free Coyoneda in scalaz 7.2

April 02, 2017

scalaz/scalaz#731 added machinery for running free monads using Coyoneda for free functors. This can be seen in action in Programs as Values from LambdaConf 2015, as well as FreeConsole.

Free was reworked in scalaz/scalaz#938 to always use this pattern, making Coyoneda "free". Here's what an updated console IO example might look like under scalaz 7.2.

Program as values

sealed trait ConsoleOp[A]
case object Readln extends ConsoleOp[String]
case class Writeln(x: String) extends ConsoleOp[Unit]

Free Coyoneda

import scalaz.Free
import scalaz.Free.liftF

type ConsoleIO[A] = Free[ConsoleOp, A]

val readln: ConsoleIO[String] = liftF(Readln)
def writeln(x: String): ConsoleIO[Unit] = liftF(Writeln(x))

Pure programs

import scalaz.effect.IO

case class Person(name: String)

val getPerson: ConsoleIO[Person] =
  for {
    _     <- writeln("What is your name?")
    name  <- readln
  } yield Person(name)

def greetPerson(person: Person): ConsoleIO[Unit] =
  writeln(s"Hello, ${person.name}!")
val program: ConsoleIO[Unit] = getPerson >>= greetPerson

Test interpreter

case class TestIO[A](io: List[String], result: A)

import scalaz.Monad

implicit val testIOMonad: Monad[TestIO] =
  new Monad[TestIO] {
    def point[A](a: => A): TestIO[A] = TestIO(Nil, a)
    def bind[A,B](fa: TestIO[A])(f: A => TestIO[B]) =
      f(fa.result) match { case TestIO(io, b) =>
        TestIO(fa.io ++ io, b)
      }
  }

import scalaz.~>

def testRunner(input: List[String]): ConsoleOp ~> TestIO =
  new (ConsoleOp ~> TestIO) {

    import scala.collection.mutable.Stack
    private val inputS = input.to[Stack]

    def apply[A](fa: ConsoleOp[A]): TestIO[A] =
      fa match {
        case Readln     => val line = inputS.pop
                           TestIO(List(line), line)
        case Writeln(x) => TestIO(List(x), ())
      }
  }

val testIO: TestIO[Unit] = program.foldMap(testRunner(List("James")))
println(testIO) // TestIO(List(What is your name?, James, Hello, James!),())

Console interpreter

val consoleRunner: ConsoleOp ~> IO =
  new (ConsoleOp ~> IO) {
    def apply[A](fa: ConsoleOp[A]): IO[A] =
      fa match {
        case Readln     => IO(readLine)
        case Writeln(x) => IO(println(x))
      }
  }

program.foldMap(consoleRunner).unsafePerformIO
// What is your name?
// James
// Hello, James!

Demo

This code can be run with sbt and codedown:

/***
libraryDependencies += "org.scalaz" %% "scalaz-core" % "7.2.10"
libraryDependencies += "org.scalaz" %% "scalaz-effect" % "7.2.10"
*/
$ curl -sL earldouglas.com/posts/freec.md | codedown scala > freec.scala
$ sbt -Dsbt.main.class=sbt.ScriptMain freec.scala
What is your name?
James
Hello, James!