Skip to content

Instantly share code, notes, and snippets.

@mattroberts297
Last active May 31, 2020 10:19
Show Gist options
  • Save mattroberts297/4a2229a7c2a44f5a339ab7328426cf87 to your computer and use it in GitHub Desktop.
Save mattroberts297/4a2229a7c2a44f5a339ab7328426cf87 to your computer and use it in GitHub Desktop.
Cats and monad transformers

Monad transformers let you cut through nested monads (“stacks of monads”):

  • Future[Either[A, Option[B]]]
  • Future[Either[A, B]]
  • Future[Option[A]]

Why you would want to do that:

  • You do not need nested for-comprehensions
  • Your code is cleaner and easier to maintain
  • You look awesome doing it

Avoid:

  • Returning monad transformers, instead return the original monads

Why:

  • Let the client code decide what to do about the monad(s)
  • Avoid importing cats everywhere
  • Consumer choice (might prefer scalaz)

Code:

import scala.concurrent.ExecutionContext
import scala.concurrent.Future
object EvenHarderExample {
import Types._
trait FooRepo {
def foo()(implicit ec: ExecutionContext): Future[Error Or Foo]
}
trait BarRepo {
def bar(): Error Or Bar
}
trait BazRepo {
def baz(): Baz
}
class Controller(fooRepo: FooRepo, barRepo: BarRepo, bazRepo: BazRepo) {
import cats.instances.future._
def foobarbaz()(implicit ec: ExecutionContext): Future[Error Or (Foo, Bar, Baz)] = {
OrT.value {
for {
foo <- OrT(fooRepo.foo())
bar <- OrT.fromOr(barRepo.bar())
baz <- OrT.pure(bazRepo.baz())
} yield {
(foo, bar, baz)
}
}
}
}
}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
object HarderExample {
import Types._
trait FooRepo {
def foo()(implicit ec: ExecutionContext): Future[Error Or Foo]
}
trait BarRepo {
def bar(): Error Or Bar
}
class Controller(fooRepo: FooRepo, barRepo: BarRepo) {
import cats.instances.future._
def foobar()(implicit ec: ExecutionContext): Future[Error Or (Foo, Bar)] = {
OrT.value {
for {
foo <- OrT(fooRepo.foo())
bar <- OrT.fromOr(barRepo.bar())
} yield {
(foo, bar)
}
}
}
}
}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
object MyEyesAreBleedingExample {
import Types._
import MyEyesAreBleedingTypes._
trait FooRepo {
def foo()(implicit ec: ExecutionContext): Future[Error Or Maybe[Foo]]
}
trait BarRepo {
def bar(): Error Or Bar
}
trait BazRepo {
def baz(): Baz
}
class Controller(fooRepo: FooRepo, barRepo: BarRepo, bazRepo: BazRepo) {
import cats.instances.future._
def foobarbaz()(implicit ec: ExecutionContext): Future[Error Or Maybe[(Foo, Bar, Baz)]] = {
OrMaybeT.value {
for {
foo <- OrMaybeT(fooRepo.foo())
bar <- OrMaybeT.fromOr(barRepo.bar())
baz <- OrMaybeT.pure(bazRepo.baz())
} yield {
(foo, bar, baz)
}
}
}
}
}
import Types.Or
import cats.Applicative
import cats.Functor
import cats.Monad
object MyEyesAreBleedingTypes {
type Maybe[+A] = Option[A]
final case class OrMaybeT[F[_], A, B](value: F[A Or Maybe[B]]) {
def map[C](f: B => C)(implicit F: Functor[F]): OrMaybeT[F, A, C] = OrMaybeT {
F.map(value) { (aOrMaybeB: Or[A, Maybe[B]]) =>
aOrMaybeB.map { (maybeB: Maybe[B]) =>
maybeB.map(f)
}
}
}
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
def flatMap[AA >: A, C](f: B => OrMaybeT[F, AA, C])(implicit F: Monad[F]): OrMaybeT[F, AA, C] =
OrMaybeT {
F.flatMap(value) {
case Right(Some(b)) => f(b).value
case orMaybe => F.pure(orMaybe.asInstanceOf[AA Or Maybe[C]])
}
}
}
object OrMaybeT {
final def value[F[_], A, B](stack: OrMaybeT[F, A, B]) = stack.value
final def pure[F[_], A, B](b: B)(implicit F: Applicative[F]): OrMaybeT[F, A, B] = OrMaybeT {
F.pure(Right(Some(b)))
}
final def fromOr[F[_]]: FromPartiallyApplied[F] = new FromPartiallyApplied
final class FromPartiallyApplied[F[_]] private[OrMaybeT] {
final def apply[E, A](or: E Or A)(implicit F: Applicative[F]): OrMaybeT[F, E, A] =
OrMaybeT(F.pure(or.map(a => Some(a))))
}
}
}
import Types.Or
object MyFaceIsMeltingTypes {
final case class OrT[F[_], G[_], A, B](value: F[A Or G[B]]) {
// TODO map and flatMap. Hint: will need type class and instance not in cats (that I know of(.
}
}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
object SimpleExample {
import Types._
trait FooRepo {
def foo()(implicit ec: ExecutionContext): Future[Error Or Foo]
}
trait BarRepo {
def bar()(implicit ec: ExecutionContext): Future[Error Or Bar]
}
class Controller(fooRepo: FooRepo, barRepo: BarRepo) {
def foobar()(implicit ec: ExecutionContext): Future[Error Or (Foo, Bar)] = {
import cats.instances.future._
OrT.value {
for {
foo <- OrT(fooRepo.foo())
bar <- OrT(barRepo.bar())
} yield {
(foo, bar)
}
}
}
}
}
import scala.concurrent.Future
import cats.Applicative
import cats.data.EitherT
import cats.data.OptionT
object Types {
final case class Error(msg: String)
final case class Foo(value: String)
final case class Bar(value: String)
final case class Baz(value: String)
type Or[+A, +B] = Either[A, B]
type OrT[F[_], A, B] = EitherT[F, A, B]
type MyEitherT[B] = EitherT[Future, Error, B]
type MyOptionT[B] = OptionT[MyEitherT, B]
object OrT {
final def apply[F[_], A, B](value: F[A Or B]) = EitherT(value)
final def value[F[_], A, B](stack: EitherT[F, A, B]) = stack.value
final def pure[F[_], A, B](b: B)(implicit F: Applicative[F]) = EitherT.pure[F, A, B](b)
final def fromOr[F[_]]: FromOrPartiallyApplied[F] = new FromOrPartiallyApplied
final class FromOrPartiallyApplied[F[_]] private[OrT] {
final def apply[E, A](or: E Or A)(implicit F: Applicative[F]): OrT[F, E, A] =
OrT(F.pure(or))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment