Last active
November 16, 2018 19:48
-
-
Save mattroberts297/e29d12550398481d01fbbb53b80e09b0 to your computer and use it in GitHub Desktop.
Monad transformers and stuff
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
scalaVersion := "2.12.0" | |
libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.0-MF" |
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
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 | |
} | |
} |
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
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