Last active
June 4, 2021 06:09
-
-
Save filippovitale/5c107b3e44bbbc616851 to your computer and use it in GitHub Desktop.
FreeConsole – Simplest end to end example of Coyoneda and Free Monad in Scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import scalaz.effect.IO | |
import scalaz.std.function._ | |
import scalaz.{Coyoneda, Free, Monad, State, ~>} | |
object NonFunctor extends App { | |
// my simple algebra | |
sealed trait Console[A] | |
case class PrintLine(msg: String) extends Console[Unit] | |
case object ReadLine extends Console[String] | |
// Let's create a functor using Coyoneda | |
// - Coyoneda will turn any F[A] into a Coyoneda[F,A] | |
// - Coyoneda[F,A] is a functor | |
type ConsoleCoyo[A] = Coyoneda[Console, A] | |
// Then scalaz's Free has a type alias just for this: | |
// - A free monad over the free functor generated by `S` | |
// - type FreeC[S[_], A] = Free[({type f[x] = Coyoneda[S, x]})#f, A] | |
// So now We have a Free monad for console: | |
type ConsoleMonad[A] = Free.FreeC[Console, A] | |
// Free monad over the free functor of ConsoleCoyo. The instance is not inferrable. | |
// Useful for the for comprehension | |
implicit val MonadConsole: Monad[ConsoleMonad] = Free.freeMonad[({type λ[α] = Coyoneda[Console, α]})#λ] | |
// smart constructor of Console[A] | |
// also you'll find handy that scalaz's Free has a function to lift a F[A] directly into a monad: | |
// A free monad over a free functor of `S` | |
// - def liftFC[S[_], A](s: S[A]): FreeC[S, A] = liftFU(Coyoneda lift s) | |
def printLine(s: String) = Free.liftFC(PrintLine(s)) | |
val readLine = Free.liftFC(ReadLine) | |
////////////////////////////////////////////////////////////////////////////// | |
val prog = for { | |
_ <- printLine("What is your name?") | |
name <- readLine | |
_ <- printLine(s"Hello: $name") | |
} yield () | |
////////////////////////////////////////////////////////////////////////// | |
case class ConsoleContext(stdin: BufferedReader, stdout: PrintWriter) | |
// Natural transformation | |
type ConsoleReader[A] = ConsoleContext => A | |
val toState: Console ~> ConsoleReader = | |
new (Console ~> ConsoleReader) { | |
def apply[A](fa: Console[A]): ConsoleReader[A] = | |
// it constrains the meaning of A into something else | |
fa match { | |
case PrintLine(s) => c => { c.stdout.println(s); c.stdout.flush() } | |
case ReadLine => _.stdin.readLine() | |
} | |
} | |
// Now we have enough structure to run a program (every function is a reader monad) | |
def runConsole[A](program: ConsoleMonad[A], consoleContext: ConsoleContext): A = | |
Free.runFC[Console, ConsoleReader, A](program)(toState).apply(consoleContext) | |
val defaultConsoleContext = ConsoleContext( | |
new BufferedReader(new InputStreamReader(System.in)), | |
new PrintWriter(System.out)) | |
implicit class ConsoleOps[A](ma: ConsoleMonad[A]) { | |
def exec(consoleContext: ConsoleContext): A = runConsole(ma, consoleContext) | |
def liftIO: IO[A] = IO(defaultConsoleContext).map(exec) | |
} | |
////////////////////////////////////////////////////////////////////////////// | |
case class PureConsoleContext(inputs: List[String], outputs: List[String]) | |
// Natural transformation | |
type PureConsoleReader[A] = State[PureConsoleContext, A] | |
val toPureState: Console ~> PureConsoleReader = | |
new (Console ~> PureConsoleReader) { | |
def apply[A](fa: Console[A]): PureConsoleReader[A] = | |
// it constrains the meaning of A into something else | |
fa match { | |
case PrintLine(s) => | |
State[PureConsoleContext, Unit] { st => | |
// TODO Lens | |
(st.copy(outputs = s::st.outputs), ()) | |
} | |
case ReadLine => | |
State[PureConsoleContext, String] { st => | |
// TODO Lens | |
st.inputs match { | |
case i :: is => st.copy(inputs = is) -> i | |
case Nil => ???; | |
} | |
} | |
} | |
} | |
// Now we have enough structure to run a program (every function is a reader monad) | |
def runPureConsole[A](program: ConsoleMonad[A], consoleContext: PureConsoleContext): (PureConsoleContext, A) = { | |
Free.runFC[Console, PureConsoleReader, A](program)(toPureState).run(consoleContext) | |
} | |
implicit class PureConsoleOps[A](ma: ConsoleMonad[A]) { | |
def exec(consoleContext: PureConsoleContext): (PureConsoleContext, A) = runPureConsole(ma, consoleContext) | |
} | |
/////////////////////////// | |
// prog.exec(defaultConsoleContext) | |
println(prog.exec(PureConsoleContext(List("Filippo"), Nil))) | |
} | |
/* | |
friendly reminder, we are doing all this to avoid: | |
trait ConsoleContextOperations { | |
def println(s: String): Unit | |
def readLine(): String | |
} | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment