Skip to content

Instantly share code, notes, and snippets.

@Ghurtchu
Last active April 1, 2023 16:59
Show Gist options
  • Save Ghurtchu/bf2026e5a830321eacb02216799bf579 to your computer and use it in GitHub Desktop.
Save Ghurtchu/bf2026e5a830321eacb02216799bf579 to your computer and use it in GitHub Desktop.
Tagless Final 101
import scala.util.Try
object TaglessFinalGuessGame {
trait Monad[F[_]] {
def pure[A](a: A): F[A]
def map[A, B](fa: F[A])(f: A => B): F[B] = flatMap(fa)((f andThen pure)(_))
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}
object Monad {
def apply[F[_] : Monad]: Monad[F] = implicitly
}
implicit class MonadSyntax[F[_]: Monad, A](self: F[A]) {
def map[B](f: A => B): F[B] = Monad[F].map(self)(f)
def flatMap[B](f: A => F[B]): F[B] = Monad[F].flatMap(self)(f)
}
implicit class ApplicativeSyntax[A](self: A) {
def pure[F[_]: Monad]: F[A] = Monad[F].pure(self)
}
final case class IO[+A](execute: () => A) {
def map[B](f: A => B): IO[B] = IO(() => f(execute()))
def flatMap[B](f: A => IO[B]): IO[B] = IO(() => f(execute()).execute())
}
object IO {
def delay[A](thunk: => A): IO[A] = IO(() => thunk)
implicit val ioMonad: Monad[IO] = new Monad[IO] {
override def pure[A](a: A): IO[A] = IO.delay(a)
override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.flatMap(f)
}
}
trait Console[F[_]] {
def write(s: String): F[Unit]
def read: F[String]
}
object Console {
def apply[F[_]: Console]: Console[F] = implicitly
implicit val ioConsole: Console[IO] = new Console[IO] {
override def write(s: String): IO[Unit] = IO.delay(println(s))
override def read: IO[String] = IO.delay(scala.io.StdIn.readLine())
}
}
trait Random[F[_]] {
def gen(from: Int, to: Int): F[Int]
}
object Random {
def apply[F[_]: Random]: Random[F] = implicitly
implicit val ioRandom: Random[IO] = (from: Int, to: Int) =>
IO.delay(scala.util.Random.between(from, to + 1))
}
final class GuessGame[F[_] : Monad : Random : Console] {
def play: F[Unit] = for {
_ <- Console[F].write("Welcome to the game")
_ <- gameLoop
} yield ()
private def gameLoop: F[Unit] = for {
_ <- Console[F].write("Choose numbers between 1 and 5")
input <- Console[F].read
msg <- Try(input.toInt).toOption.fold[F[String]]("Incorrect input, please write number".pure[F]) { guess =>
for {
rand <- Random[F].gen(1, 5)
msg <- (if (rand == guess) "You guessed it" else "You didn't guess it").pure[F]
} yield msg
}
_ <- Console[F].write(msg)
_ <- Console[F].write("Do you want to play again? y/n")
_ <- Console[F].read.flatMap {
case "yes" | "y" => gameLoop
case "no" | "n" => Console[F].write("bye!")
case _ => Console[F].write("incorrect input")
}
} yield ()
}
def main(args: Array[String]): Unit = {
val game = new GuessGame[IO]
game.play.execute()
}
}
@Ghurtchu
Copy link
Author

Tagless final encoding is a way to achieve effect polymorphism in Scala. There are three things we need to pay attention while practicing it:

  • algebras : F[_] trait with a set of operations
  • interpreters : concrete implementations of algebras
  • programs : conjunction of one or more interpreters to achieve something

Console algebra:

trait Console[F[_]] { 
  def write(s: String): F[Unit]
  def read: F[String]
}

Console interpreter:

class RealConsole[F[_]: Async] extends Console[F] {
  override def write(s: String): F[Unit] = Async[F].delay(println(s))
  override def read: F[String] = Async[F].delay(scala.io.StdIn.readLine())
}

Console program:

import cats.syntax.all._

class HelloConsoleProgram[F[_]: Monad](console: Console[F]) {
  def readAndGreet: F[Unit] =
    for {
      _    <- console.write("What is your name?")
      name <- console.read
      _    <- console.write(s"Hello $name")
    } yield ()
}

main method which parameterises program with concrete effect F[_] - [cats.effect.IO]

override def run(args: List[String]): IO[ExitCode] = 
  new HelloConsoleProgram[IO](new RealConsole[IO])
    .readAndGreet.as(ExitCode.Success)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment