Last active
April 1, 2023 16:59
-
-
Save Ghurtchu/bf2026e5a830321eacb02216799bf579 to your computer and use it in GitHub Desktop.
Tagless Final 101
This file contains 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 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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Tagless final encoding is a way to achieve effect polymorphism in Scala. There are three things we need to pay attention while practicing it:
F[_]
trait with a set of operationsConsole algebra:
Console interpreter:
Console program:
main method which parameterises program with concrete effect F[_] - [cats.effect.IO]