Last active
April 2, 2023 17:34
-
-
Save Ghurtchu/748357ddcca9c571553d15bbc45c5194 to your computer and use it in GitHub Desktop.
OptionT - Option Monad Transformer
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
object OptionTransformerImpl { | |
final case class OptionT[F[_] : Monad, A](value: F[Option[A]]) { | |
def map[B](f: A => B): OptionT[F, B] = | |
OptionT(value.map(_.map(f))) | |
def flatMap[B](f: A => OptionT[F, B]): OptionT[F, B] = | |
OptionT(value.flatMap(_.fold(Option.empty[B].pure[F])(a => f(a).value))) | |
} | |
object OptionT { | |
def liftF[F[_]: Monad, A](fa: F[A]): OptionT[F, A] = OptionT(fa.map(Some(_))) | |
def fromOption[F[_]: Monad, A](oa: Option[A]): OptionT[F, A] = OptionT(oa.pure[F]) | |
} | |
final case class IO[A](unsafeRun: () => A) { | |
def map[B](f: A => B): IO[B] = IO.delay(f(unsafeRun())) | |
def flatMap[B](f: A => IO[B]): IO[B] = IO.delay(f(unsafeRun()).unsafeRun()) | |
} | |
object IO { | |
def delay[A](a: => A): IO[A] = new IO(() => a) | |
implicit val ioMonad: Monad[IO] = new Monad[IO] { | |
override def pure[A](a: A): IO[A] = IO.delay(a) | |
override def map[A, B](fa: IO[A])(f: A => B): IO[B] = fa.map(f) | |
override def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.flatMap(f) | |
} | |
} | |
trait Monad[F[_]] { | |
def pure[A](a: A): F[A] | |
def map[A, B](fa: F[A])(f: A => B): F[B] | |
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 PureSyntax[A](self: A) { | |
def pure[F[_]: Monad]: F[A] = Monad[F].pure(self) | |
} | |
object api { | |
final case class User(id: String, email: String, name: String) | |
final case class Letter(from: User, to: User, body: String) | |
def getUser(id: String): IO[Option[User]] = IO.delay { | |
Option(User(id, "_CatamorphisT_" ,"Leo")) | |
} | |
def validate(user: User): Option[User] = | |
Option.when(user.email.nonEmpty)(user) | |
def generateLetter(user: User): Letter = | |
Letter( | |
from = User("1", "admin@gmail.com", "admin"), | |
to = user, | |
body = s"Welcome to our website, ${user.name}!" | |
) | |
} | |
def main(args: Array[String]): Unit = { | |
import api._ | |
val result: IO[Option[(User, Letter)]] = (for { | |
user <- OptionT(getUser("1337")) | |
validUser <- OptionT.fromOption[IO, User](validate(user)) | |
letter <- OptionT.liftF(generateLetter(validUser).pure[IO]) | |
} yield (user, letter)).value | |
println(result.unsafeRun()) | |
} | |
} |
With monad transformers:
val result: IO[Option[(User, Letter)]] =
(for {
user <- OptionT(getUser("1337"))
validUser <- OptionT.fromOption[IO, User](validate(user))
letter <- OptionT.liftF(generateLetter(validUser).pure[IO])
} yield (user, letter)).value
Without monad transformers we would need to write such a boilerplate:
val res: IO[Option[(User, Letter)]] = for {
maybeUser <- getUser("1337")
maybeUserAndLetter <- maybeUser match {
case Some(user) => validate(user) match {
case Some(validUser) => Some {
(validUser, generateLetter(validUser))
}.pure[IO]
case _ => None.pure[IO]
}
case _ => None.pure[IO]
}
} yield maybeUserAndLetter
^ and that on a good day 😄
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Monad transformers help us to avoid boilerplate when dealing with nested monads - monads wrapped by monads.