Skip to content

Instantly share code, notes, and snippets.

@Ghurtchu
Last active April 2, 2023 17:34
Show Gist options
  • Save Ghurtchu/748357ddcca9c571553d15bbc45c5194 to your computer and use it in GitHub Desktop.
Save Ghurtchu/748357ddcca9c571553d15bbc45c5194 to your computer and use it in GitHub Desktop.
OptionT - Option Monad Transformer
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())
}
}
@Ghurtchu
Copy link
Author

Ghurtchu commented Mar 31, 2023

Monad transformers help us to avoid boilerplate when dealing with nested monads - monads wrapped by monads.

@Ghurtchu
Copy link
Author

Ghurtchu commented Mar 31, 2023

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