Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Typelevel Mouse to the rescue

Typelevel Mouse to the rescue

We all know that EitherT/OptionT can oftentimes make our code less readable. But is there actually any alternative? Meet the Typelevel Mouse - a tiny library you've probably never heard of that can help you when you want to avoid using EitherT and OptionT

Avatar for Przemysław Sokół

Przemysław Sokół

September 01, 2022

Other Decks in Programming

Transcript

  1. Mouse provides utils for... • Any • Boolean • Double

    • F[A] for any F, A • F[Either[A, B]] • F[Option[A]] • F[G[A]] • F[G[H[A]]] • F[TupleN] • Int • Long • Map • Option • String • Try • Tuple
  2. IO[_] object Main extends App { val future = Future(println("Hi!"))

    for { _ <- future _ <- future } yield () // → Hi! }
  3. IO[_] object Main extends App { val io = IO(println("Hello!"))

    val app = for { _ <- io _ <- io } yield () app.unsafeRunSync() // → Hello! // → Hello! }
  4. IO[_] object Main extends IOApp { val io = IO(println("Hello!"))

    def run(args: List[String]): IO[ExitCode] = (for { _ <- io _ <- io } yield ()).as(ExitCode.Success) // → Hello! // → Hello! }
  5. EitherT def parseDoubleAsync(s: String): Future[Either[String, Double]] = ... def divideAsync(a:

    Double, b: Double): Future[Either[String, Double]] = ... def divisionProgramAsync(inputA: String, inputB: String): Future[Either[String, Double]] = parseDoubleAsync(inputA) flatMap { eitherA => parseDoubleAsync(inputB) flatMap { eitherB => (eitherA, eitherB) match { case (Right(a), Right(b)) => divideAsync(a, b) case (Left(err), _) => Future.successful(Left(err)) case (_, Left(err)) => Future.successful(Left(err)) } } }
  6. EitherT def parseDoubleAsync(s: String): Future[Either[String, Double]] = ... def divideAsync(a:

    Double, b: Double): Future[Either[String, Double]] = ... def divisionProgramAsync(inputA: String, inputB: String): EitherT[Future, String, Double] = for { a <- EitherT(parseDoubleAsync(inputA)) b <- EitherT(parseDoubleAsync(inputB)) result <- EitherT(divideAsync(a, b)) } yield result
  7. sealed trait Error case object MailNotDelivered extends Error case object

    MailMissing extends Error case object NoAttachment extends Error case object ... extends Error
  8. EitherT def parseUsername(name: String): Either[Error, Username] = ... def getUserId(username:

    Username): IO[Either[Error, UserId]] = ... def getLastMail(userId: UserId): IO[Option[Mail]] = ... def firstAttachment(mail: Mail): Option[Attachment] = ... (for { username <- EitherT.fromEither[IO](parseUsername("Alice")) userId <- EitherT(getUserId(username)) mail <- EitherT.fromOptionF(getLastMail(userId), ifNone = MailMissing) _ <- EitherT.cond[IO](test = mail.status == Delivered, right = unit, left = MailNotDelivered) result <- EitherT.fromOption[IO](firstAttachment(mail), ifNone = NoAttachment).leftWiden[Error] } yield result).value
  9. EitherT.cond // with parameter names: EitherT.cond[IO](test = mail.status == Delivered,

    right = unit, left = MailNotDelivered) // ...and without: EitherT.cond[IO](mail.status == Delivered, unit, MailNotDelivered) // one could expect this: EitherT.cond[IO](..., LEFT, RIGHT) // ...but it's actually: EitherT.cond[IO](..., RIGHT, LEFT)
  10. Mouse def parseUsername(name: String): Either[Error, Username] = ... def getUserId(username:

    Username): IO[Either[Error, UserId]] = ... def getLastMail(userId: UserId): IO[Option[Mail]] = ... def firstAttachment(mail: Mail): Option[Attachment] = ... parseUsername("Alice") .pure[IO] .flatMapF(getUserId) .flatMapF { userId => getLastMail(userId).toRightIn(left = MailMissing) } .flatMapIn { mail => if (mail.status == Delivered) mail.asRight else MailNotDelivered.asLeft } .flatMapIn(firstAttachment(_).toRight(left = NoAttachment))
  11. Mouse F[Either[L, R]] def mapIn[A](f: R => A)(implicit F: Functor[F]):

    F[Either[L, A]] def flatMapIn[A >: L, B](f: R => Either[A, B])(implicit F: Functor[F]): F[Either[A, B]] def flatMapF[A >: L, B](f: R => F[Either[A, B]])(implicit F: Monad[F]): F[Either[A, B]] def getOrElseF[A >: R](right: => F[A])(implicit F: Monad[F]): F[A] def getOrRaise[E](e: => E)(implicit F: MonadError[F, _ >: E]): F[R] def getOrRaiseMsg(msg: => String)(implicit F: MonadThrow[F]): F[R] (...)
  12. Mouse F[Either[L, R]] def mapIn[A](f: R => A)(...): F[Either[L, A]]

    def flatMapIn[A >: L, B](f: R => Either[A, B])(...): F[Either[A, B]] def flatMapF[A >: L, B](f: R => F[Either[A, B]])(...): F[Either[A, B]] def getOrElseF[A >: R](right: => F[A])(...): F[A] def getOrRaise[E](e: => E)(...): F[R] def getOrRaiseMsg(msg: => String)(...): F[R] (...)
  13. Mouse F[Option[A]] def mapIn[B](f: A => B)(...): F[Option[B]] def flatMapIn[B](f:

    A => Option[B])(...): F[Option[B]] def flatMapF[B](f: A => F[Option[B]])(...): F[Option[B]] def getOrElseF[B >: A](fa: => F[B])(...): F[B] (...)
  14. EitherT def getA: IO[Either[_, Int]] = ... def getB: IO[Either[_,

    String]] = ... def foo(a: Int, b: String): IO[Either[_, Bar]] = ... for { a <- EitherT(getA) b <- EitherT(getB) result <- EitherT(foo(a, b)) } yield result
  15. Mouse def getA: IO[Either[_, Int]] = ... def getB: IO[Either[_,

    String]] = ... def foo(a: Int, b: String): IO[Either[_, Bar]] = ... getA.flatMapF { a => getB.flatMapF { b => foo(a, b) } }
  16. ...more from Mouse "1.0".parseFloat // → Right(1.0F) Map(1 -> 2,

    3 -> 4).mapKeys(_ * 2) // → Map(2 -> 2, 6 -> 4) List(Option(1), Option(2)).mapNested2(_ * 2) // → List(Some(2), Some(4)) val either: Either[L, R] = ... either.toEitherT[IO] // → EitherT[IO, L, R] (1, 2, 4, 8).last // → 8