Skip to content

Instantly share code, notes, and snippets.

@mattroberts297
Last active November 16, 2018 19:48
Show Gist options
  • Save mattroberts297/e29d12550398481d01fbbb53b80e09b0 to your computer and use it in GitHub Desktop.
Save mattroberts297/e29d12550398481d01fbbb53b80e09b0 to your computer and use it in GitHub Desktop.
Monad transformers and stuff
scalaVersion := "2.12.0"
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.0-MF"
package data
import cats.Functor
import cats.Monad
import cats.syntax.either._
final case class EitherOptionT[F[_], A, B](value: F[Either[A, Option[B]]]) {
def map[C](f: B => C)(implicit F: Functor[F]): EitherOptionT[F, A, C] =
EitherOptionT(F.map(value)(e => e.map(o => o.map(f))))
def flatMap[C](f: B => EitherOptionT[F, A, C])(implicit F: Monad[F]): EitherOptionT[F, A, C] =
EitherOptionT(
F.flatMap(value) {
case l @ Left(_) => F.pure(l.rightCast[Option[C]])
case Right(o) => o match {
case None => F.pure(Right[A, Option[C]](Option.empty))
case Some(b) => f(b).value
}
}
)
}
object EitherOptionT {
def value[F[_], A, B](
wrapped: EitherOptionT[F, A, B]
): F[Either[A, Option[B]]] = {
wrapped.value
}
}
import scala.concurrent.ExecutionContext
import scala.concurrent.Future
import cats.instances.future._
import cats.data.OptionT
import cats.data.EitherT
import data.EitherOptionT
final case class UserId(value: String) extends AnyVal
final case class PostId(value: String) extends AnyVal
final case class User(id: UserId, postIds: List[PostId], name: String)
final case class Post(id: PostId, body: String)
final case class WordCountStats(mean: Double, min: Double, max: Double)
final case class Error(message: String)
object Types {
type Or[+A, +B] = Either[A, B]
type \/[+A, +B] = Either[A, B]
type FutureErrorOr[A] = EitherT[Future, Error, A]
object FutureErrorOr {
def apply[A](value: Future[Either[Error, A]]): FutureErrorOr[A] = EitherT(value)
def value[A](wrapped: FutureErrorOr[A]): Future[Either[Error, A]] = wrapped.value
}
type FutureErrorOrOption[A] = OptionT[FutureErrorOr, A]
object FutureErrorOrOption {
def apply[A](
value: Future[Either[Error, Option[A]]]
): FutureErrorOrOption[A] = {
OptionT(FutureErrorOr(value))
}
def value[A](
wrapped: FutureErrorOrOption[A]
): Future[Either[Error, Option[A]]] = {
FutureErrorOr.value(wrapped.value)
}
}
// What about:
//
// type EitherOptionT[A, B] = OptionT[EitherT[Future, A, Option[B]], B]
//
// This doesn't work because OptionT's first parameter should have one
// parameter, but EitherT has three i.e. F[_] versus EitherT[_, _, _], so to
// be generic on A (Error) then a new class is needed. That is why in the
// above the type aliases all have to take one parameter.
}
object FutureEitherExample {
import Types._
trait Users {
def getUser(id: UserId): Future[Error Or User]
}
trait Posts {
def getPosts(ids: List[PostId]): Future[Error Or List[Post]]
}
trait Stats {
def calculate(posts: List[Post]): Future[Error Or WordCountStats]
}
class Service(users: Users, posts: Posts, stats: Stats) {
def statsFor(userId: UserId)(implicit context: ExecutionContext): Future[Error Or WordCountStats] = {
val stack: FutureErrorOr[WordCountStats] = for {
user <- FutureErrorOr(users.getUser(userId))
posts <- FutureErrorOr(posts.getPosts(user.postIds))
wordCountStats <- FutureErrorOr(stats.calculate(posts))
} yield wordCountStats
stack.value
}
}
}
object FutureEitherOptionExample {
import Types._
trait Users {
def read(id: UserId): Future[Error Or Option[User]]
}
trait Posts {
def read(ids: List[PostId]): Future[Error Or Option[List[Post]]]
}
trait Stats {
def read(posts: List[Post]): Future[Error Or Option[WordCountStats]]
}
class Service(users: Users, posts: Posts, stats: Stats) {
def statsFor(userId: UserId)(implicit context: ExecutionContext): Future[Either[Error, Option[WordCountStats]]] = {
FutureErrorOrOption.value {
for {
user <- FutureErrorOrOption(users.read(userId))
posts <- FutureErrorOrOption(posts.read(user.postIds))
wordCountStats <- FutureErrorOrOption(stats.read(posts))
} yield wordCountStats
}
}
}
}
object FutureEitherOptionTExample {
import Types._
trait Users {
def read(id: UserId): Future[Error Or Option[User]]
}
trait Posts {
def read(ids: List[PostId]): Future[Error Or Option[List[Post]]]
}
trait Stats {
def read(posts: List[Post]): Future[Error Or Option[WordCountStats]]
}
class Service(users: Users, posts: Posts, stats: Stats) {
def statsFor(userId: UserId)(implicit context: ExecutionContext): Future[Either[Error, Option[WordCountStats]]] = {
EitherOptionT.value {
for {
user <- EitherOptionT(users.read(userId))
posts <- EitherOptionT(posts.read(user.postIds))
wordCountStats <- EitherOptionT(stats.read(posts))
} yield wordCountStats
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment