Using monads to enforce programming style your Boss likes
trait Service[M[_]] {
def repo: Repository[M]
def doSth(request: Request)(implicit ev: Monad[M]): M[Either[Error, Result]] =
EitherT
.fromOptionF(repo.findSomeData(request.computeCondition), ifNone = NotFound: Error)
.ensure(onFailure = NotValid)(_.isValid)
.flatMapF(dto => updateDto(dto.update(request.computeNewValue)))
.semiflatMap(dto => inspectData(request, dto).tupleLeft(dto))
.map {
case (dto, (Some(x), xs)) if xs.contains(x) => EvenBetter(dto, x)
case (dto, _) => Ok(dto)
}
.value
private def inspectData(request: Request, dto: DTO)(implicit ev: Applicative[M]) =
(repo.findSomeRelatedData(dto),
repo.filterSomething(dto, request.computeAnotherCondition)).tupled
private def updateDto(dto: DTO)(implicit ev: Functor[M]) =
repo.update(dto).map(updated => Either.cond(updated == 1, dto, StarsAreNotRight: Error))
}
But EitherT is nice. Look ...
trait Service[M[_]] {
def repo: Repository[M]
def doSth(request: Request)(implicit ev: Monad[M]): M[Either[Error, Result]] =
EitherT
.fromOptionF(repo.findSomeData(request.computeCondition), ifNone = NotFound: Error)
.ensure(onFailure = NotValid)(_.isValid)
.flatMapF(dto => updateDto(dto.update(request.computeNewValue)))
.semiflatMap(dto => inspectData(request, dto).tupleLeft(dto))
.map {
case (dto, (Some(x), xs)) if xs.contains(x) => EvenBetter(dto, x)
case (dto, _) => Ok(dto)
}
.value
private def inspectData(request: Request, dto: DTO)(implicit ev: Applicative[M]) =
(repo.findSomeRelatedData(dto),
repo.filterSomething(dto, request.computeAnotherCondition)).tupled
private def updateDto(dto: DTO)(implicit ev: Functor[M]) =
repo.update(dto).map(updated => Either.cond(updated == 1, dto, StarsAreNotRight: Error))
}
But EitherT is nice. Look ...
All the nice combinators
All the nice combinators are there for smooth "integration":
- flatMapF - with a piece of code that may "throw"
- semiflatMap - with a piece of code that never "throws"
- subflatMap/ensure/ensureOr - validations
- recover / transform - error recovery
- mapK - for switching effects
But EitherT is nice. Look ...
What's not to like?
- it doesn't yield (pun intended) itself easily to for-comprehension style. (So what, I'm asking? But, people tend to feel really comfortable writing fors)
- You need to annotate types quite a lot (You're writing Scala, get used to it)
What's not to like, boss?
I forgot to mention. This won't compile. (Easily fixable)
What's not to like, boss?
sealed trait Error
object Error {
case object NotFound extends Error
case object NotValid extends Error
case object StarsAreNotRight extends Error
}
type mismatch;
found : M[Either[A$A4.this.Error,Product with Serializable with A$A4.this.Result]]
required: M[Either[A$A4.this.Error,A$A4.this.Result]]
Note: Either[A$A4.this.Error,Product with Serializable with A$A4.this.Result] <: Either[A$A4.this.Error,A$A4.this.Result], but type M is invariant in type _.
You may wish to define _ as +_ instead. (SLS 4.5)
What's not to like, boss?
sealed trait ComposedErrors
sealed trait Module1Errors extends ComposedErrors
sealed trait Module2Errors extends ComposedErrors
trait Service1[M[_]] {
def doSth: M[Either[Module1Errors, Result]]
}
trait Service2[M[_]] {
def doSth: M[Either[Module2Errors, Result]]
}
def composition[M[_]: Monad](implicit service1: Service1[M], service2: Service2[M]) =
for {
r1 <- EitherT(service1.doSth)
r2 <- EitherT(service2.doSth)
} yield (r1, r2)
What's not to like, boss?
def composition[M[_]: Monad](implicit service1: Service1[M], service2: Service2[M]) =
for {
r1 <- EitherT[M, ComposedErrors, Result](service1.doSth)
r2 <- EitherT[M, ComposedErrors, Result](service2.doSth)
} yield (r1, r2)
What's not to like, boss?
def composition[M[_]: Monad](implicit service1: Service1[M], service2: Service2[M]) =
for {
r1 <- EitherT(service1.doSth).leftMap(identity[ComposedErrors])
r2 <- EitherT(service2.doSth)
} yield (r1, r2)
What's not to like, boss?
def composition[M[_]: Monad](implicit service1: Service1[M], service2: Service2[M]) =
for {
r1 <- EitherT(service1.doSth)
r2 <- EitherT(service2.doSth).leftMap(identity[ComposedErrors])
} yield (r1, r2)
It's a bit ironic that problems with OOP is solved by dead-simple FP (leftMap with identity)
from https://www.benjamin.pizza/posts/2019-01-11-the-fourth-type-of-variance.html
What's not to like, boss?
Haskell doesn't have much better story in this regard :-(
type (+) = Either
infixr + 5
l :: l -> Either l r
l = Left
r :: r -> Either l r
r = Right
foo :: String
-> Either
(HeadError + LookupError + ParseError)
Integer
foo str = do
c <- mapLeft l (head str)
r <- mapLeft (r . l) (lookup str strMap)
mapLeft (r . r) (parse (c : r))
From Matt Parsons blog https://www.parsonsmatt.org/2018/11/03/trouble_with_typed_errors.html
What's not to like, boss?
Haskell doesn't have much better story in this regard :-(
But at least type inference works there! It'd be awesome to have a sum type with order independence and easy composition ("open")
x :: Either (HeadError + LookupError) Int
y :: Either (LookupError + HeadError) Int
What's not to like, boss?
So Haskellers admittedly adopt prisms to solve this. Check Matt's blog!
module Parser : sig
type error = [
| `ParserSyntaxError of int
| `ParserGrammarError of int * string
]
val parse : string -> (tree, [> error]) result
end
module Validation : sig
type error = [
| `ValidationLengthError of int
| `ValidationHeightError of int
]
val perform : tree -> (tree, [> error]) result
end
module Display : sig
type error = [
| `DisplayError of string
]
val render : tree -> (string, [> error]) result
end
What's not to like, boss?
Example by Vladimir Keleshev
let main source =
let open Result.Let_syntax in
let%bind tree = Parser.parse source in
let%bind tree = Validation.perform tree in
Display.render tree
val main : string -> (tree, [>
| `ParserSyntaxError of int
| `ParserGrammarError of int * string
| `ValidationLengthError of int
| `ValidationHeightError of int
| `DisplayError of string
]) result
What's not to like, boss?
Example by Vladimir Keleshev
IMO OCaml has the best solution with its polymorphic variants. Let's move on to ...
MonadError is also nice. Look ...
MonadError abstract over monads that can handle errors. For instance, MonadError[M, E] exists for:
- <M={Future, DBIO}, E = Throwable> (because both Future and DBIO can handle Throwables)
- <M = M', E = Either[E', ?]> (because any monad can handle Either as error)
- <M = EitherT[M', E', ?], E = E'> (as above)
Technically, MonadError extends Monad by adding new combinators for error handling, such as: recover, ensure, adapt, raiseError
trait Service[M[_]] {
//...
def doSth(request: Request)(implicit ME: MonadError[M, Error]): M[Result] =
for {
maybeDto <- repo.findSomeData(request.computeCondition)
dto <- ME.fromOption(maybeDto, ifEmpty = NotFound)
.ensure(error = NotValid)(_.isValid)
updatedDto = dto.update(request.computeNewValue)
_ <- updateDto(updatedDto).rethrow
res <- inspectData(request, updatedDto)
} yield
res match {
case (Some(x), xs) if xs.contains(x) => EvenBetter(updatedDto, x)
case _ => Ok(updatedDto)
}
//...
}
MonadError is also nice. Look ...
What's not to like?
- it does yield (pun intended) itself easily to for-comprehension style!
- Error type is explicit in MonadError (good for inference in flatMap)
What's not to like, boss?
What's not to like?
- this technique is very invasive (and as such not really suitable to be applied incrementally to an old code-base) as it changes all the signatures (all the methods that returned M[X], start to return eg. EitherT[M, E, X]).
// old def doSth(request: Request)(implicit ev: Monad[M]): M[Either[Error, Result]]
// new
def doSth(request: Request)(implicit ME: MonadError[M, Error]): M[Result]
What's not to like, boss?
What's not to like?
- Luka Jacobowitz's critique "Rethinking MonadError" (very instructive and recommended read)
What's not to like, boss?
trait MonadError[F[_], E] extends Monad[F] {
//...
def attempt[A](fa: F[A]): F[Either[E, A]]
/* there is no way the outer F still has any errors,
so why does it have the same type?
*/
def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
/* if the errors are handled, why does it return the exact same type?
what happens if I have errors in the E => F[A] function?
*/
}
What's not to like?
- MonadError is not composable at all wrt to error type
- Existence of MonadError[M, E1 | E2] does not imply that MonadError[M, E{1,2}] exists!
- I tried to "fix" that in this issue but there were some serious concerns and it never got merged
- The idea is that it should be possible to derive an instance of MonadError[M, E1] from MonadError[M, E] if we can prove that E1<~< E, where <~< is Liskov relationship (substitutability). It holds whenever E1 could be used in any negative context that expects an E
What's not to like, boss?
What's not to like, boss?
implicit def liskovMonadError[M[_], E1, E](implicit ME: MonadError[M, E],
liskov: Liskov[E1, E],
E1: ClassTag[E1]): MonadError[M, E1] =
new MonadError[M, E1]{
override def raiseError[A](e1: E1) = ME.raiseError(liskov.coerce(e1))
override def handleErrorWith[A](fa: M[A])(f: E1 => M[A]) = ME.recoverWith(fa) {
case e1: E1 => f(e1)
}
override def pure[A](x: A) = ME.pure(x)
override def flatMap[A, B](fa: M[A])(f: A => M[B]) = ME.flatMap(fa)(f)
override def tailRecM[A, B](a: A)(f: A => M[Either[A, B]]) = ME.tailRecM(a)(f)
}
What's not to like, boss?
implicit def liskovMonadError[M[_], E1, E](implicit ME: MonadError[M, E],
liskov: Liskov[E1, E],
E1: ClassTag[E1]): MonadError[M, E1] =
new MonadError[M, E1]{
override def raiseError[A](e1: E1) = ME.raiseError(liskov.coerce(e1))
override def handleErrorWith[A](fa: M[A])(f: E1 => M[A]) = ME.recoverWith(fa) {
case e1: E1 => f(e1)
}
override def pure[A](x: A) = ME.pure(x)
override def flatMap[A, B](fa: M[A])(f: A => M[B]) = ME.flatMap(fa)(f)
override def tailRecM[A, B](a: A)(f: A => M[Either[A, B]]) = ME.tailRecM(a)(f)
}
Shady parts
Issues with Liskov:
- any E that is not an E1 cannot be recovered (OTOH, you cannot raise such an error either)
-
MonadError[F, E1.type].handleErrorWith(Left(new E))(_ => Right(10)) ?
Whether or not this is the right thing is debatable - after all MonadError[M, E1] promises to handle only E1 errors in the context of M.
What's not to like, boss?
object dto {
case class EmailPasswordLoginRequest(email: String, password: String)
sealed trait LoginResponse
object LoginResponse {
final case class LoggedIn(token: String) extends LoginResponse
final case class AccountNeedsConfirmation(weakToken: String) extends LoginResponse
case object UserNotFound extends LoginResponse
case object AuthMethodFailure extends LoginResponse
case object InvalidCredentials extends LoginResponse
case object Deleted extends LoginResponse
}
}
A new hope
This style (single ADT for all responses) precludes usage of MonadError and is a bit awkward to use with Either.
userRepository.find(email).flatMap {
case None => M.pure(LoginResponse.InvalidCredentials)
case Some(user) if user.archivedAt.isDefined => M.pure(LoginResponse.Deleted)
case Some(user) =>
val authMethod = authMethodFromUserIdF(user.id)
val actionT = OptionT(authMethodRepository.find(user.id, authMethod.provider))
.map(checkAuthMethodAction(_))
actionT.value flatMap {
case Some(true) => M.pure(LoginResponse.LoggedIn(issueTokenFor(user)))
case Some(false) => M.pure(LoginResponse.InvalidCredentials)
case None => authMethod.mailToken match {
case Some(token) =>
sendConfirmationEmail(token, request.loginUrl, request.email)
.map(_ => LoginResponse.AccountsMergeRequested)
case None => M.pure(LoginResponse.InvalidCredentials)
}
}
}
A new hope
userRepository.find(email).flatMap {
case None => M.pure(LoginResponse.InvalidCredentials)
case Some(user) if user.archivedAt.isDefined => M.pure(LoginResponse.Deleted)
case Some(user) =>
val authMethod = authMethodFromUserIdF(user.id)
val actionT = OptionT(authMethodRepository.find(user.id, authMethod.provider))
.map(checkAuthMethodAction(_))
actionT.value flatMap {
case Some(true) => M.pure(LoginResponse.LoggedIn(issueTokenFor(user)))
case Some(false) => M.pure(LoginResponse.InvalidCredentials)
case None => authMethod.mailToken match {
case Some(token) =>
sendConfirmationEmail(token, request.loginUrl, request.email)
.map(_ =>
LoginResponse.AccountsMergeRequested)
case None => M.pure(LoginResponse.InvalidCredentials)
}
}
}
A new hope
val userT = for {
method <- EitherT.fromOptionF(findAuthMethod(token),
ifNone = ConfirmResponse.MethodNotFound)
user <- EitherT.fromOptionF(findUser(method.userId),
ifNone = ConfirmResponse.UserNotFound: ConfirmResponse)
} yield (method, user)
userT.semiflatMap {
case (method, user) => upsertAuthMethod(confirmMethod(method))
.map(_ => ConfirmResponse.Confirmed(issueTokenFor(user)))
}.merge
A new hope
val userT = for {
method <- EitherT.fromOptionF(findAuthMethod(token),
ifNone = ConfirmResponse.MethodNotFound)
user <- EitherT.fromOptionF(findUser(method.userId),
ifNone = ConfirmResponse.UserNotFound: ConfirmResponse)
} yield (method, user)
userT.semiflatMap {
case (method, user) => upsertAuthMethod(confirmMethod(method))
.map(_ => ConfirmResponse.Confirmed(issueTokenFor(user)))
}.merge
A new hope
A new hope
Favored style distilled:
- Computations form a tree (like FreeMonad)
- Leaves (results) - F[ADT]
- Nodes (intermediate values) - F[A]
- When you reach a leaf, you're done (short-circuit)
- flatMap makes a branch
- combinators for manipulating nodes (values) - just like EitherT
- for added type safety - you HAVE TO prove you have reached ADT to be able to run a computation
Let's call it "Sealed"
sealed abstract class Free[S[_], A]
final case class Pure[S[_], A](a: A) extends Free[S, A]
final case class Suspend[S[_], A](a: S[A]) extends Free[S, A]
final case class FlatMapped[S[_], B, C](c: Free[S, C], f: C => Free[S, B]) extends Free[S, B]
Our computation looks almost like Free[F[_], Either[ADT, A]]. "Almost", because we want structure to be more restricted: "reach ADT or die". Covariance on A would be also nice.
But it sure looks like a good basis
Let's call it "Sealed"
sealed abstract class Sealed[F[_], +A, ADT] {
def run(implicit ev: A <:< ADT): F[ADT]
def map[B](f: A => B): Sealed[F, B, ADT]
def flatMap[B](f: A => Sealed[F, B, ADT]): Sealed[F, B, ADT]
}
final class Result[F[_], ADT](result: F[ADT]) extends Sealed[F, Nothing, ADT]
final class Value[F[_], A, ADT](fa: F[A]) extends Sealed[F, A, ADT]
final class Computation[F[_], A0, A, ADT](fa0: F[A0],
cont: A0 => Sealed[F, A, ADT])
extends Sealed[F, A, ADT]
Let's call it "Sealed"
sealed abstract class Sealed[F[_], +A, ADT] {
def run(implicit ev: A <:< ADT, F: FlatMap[F]): F[ADT]
def map[B](f: A => B)(implicit F: Functor[F]): Sealed[F, B, ADT]
def flatMap[B](f: A => Sealed[F, B, ADT]): Sealed[F, B, ADT]
}
final class Result[F[_], ADT](result: F[ADT]) extends Sealed[F, Nothing, ADT] {
override def run(implicit ev: Nothing <:< ADT, F: FlatMap[F]) = result
override def map[B](f: Nothing => B)(implicit F: Functor[F]) = this
override def flatMap[B](f: Nothing => Sealed[F, B, ADT]) = this
}
final class Value[F[_], A, ADT](fa: F[A]) extends Sealed[F, A, ADT] {
override def run(implicit ev: A <:< ADT, F: FlatMap[F]): F[ADT] = F.fmap(fa)(ev)
override def map[B](f: A => B)(implicit F: Functor[F]) = new Value(F.fmap(fa)(f))
override def flatMap[B](f: A => Sealed[F, B, ADT]) = new Computation(fa, f)
}
final class Computation[F[_], A0, A, ADT](fa0: F[A0],
cont: A0 => Sealed[F, A, ADT])
extends Sealed[F, A, ADT] {
override def run(implicit ev: A <:< ADT, F: FlatMap[F]) =
F.flatMap(fa0)(a0 => cont(a0).run)
override def map[B](f: A => B)(implicit F: Functor[F]) =
new Computation(fa0, (a0: A0) => cont(a0).map(f))
override def flatMap[B](f: A => Sealed[F, B, ADT]) =
new Computation(fa0, (a0: A0) => cont(a0).flatMap(f))
}
Let's call it "Sealed"
val n = 50000
@scala.annotation.tailrec
def loop(s: Sealed[F, Int, Int], i: Int = 0): Sealed[F, Int, Int] =
if (i < n) loop(s.flatMap(i => new Value(Monad[F].pure(i + 1))), i + 1) else s
val s = new Value[F, Int, Int](Monad[F].pure(0))
val res = loop(s)
res.run
/*
java.lang.StackOverflowError
at Computation.$anonfun$flatMap$1(...)
at Computation.$anonfun$flatMap$1(...)
at Computation.$anonfun$flatMap$1(...)
...
*/
Oh, ok, I know - we need a trampoline ...
Let's call it "Sealed"
final class Value[F[_], A, ADT](deferred: Eval[F[A]]) extends Sealed[F, A, ADT] {
override def run(implicit ev: A <:< ADT, F: FlatMap[F]): F[ADT] =
F.fmap(deferred.value)(ev)
override def map[B](f: A => B)(implicit F: Functor[F]) =
new Value(deferred.map(fa => F.fmap(fa)(f)))
override def flatMap[B](f: A => Sealed[F, B, ADT]) =
new Computation(deferred, (a: A) => Eval.later(f(a)))
}
final class Computation[F[_], A0, A, ADT](deferred: Eval[F[A0]],
cont: A0 => Eval[Sealed[F, A, ADT]])
extends Sealed[F, A, ADT] {
override def run(implicit ev: A <:< ADT, F: FlatMap[F]) =
F.flatMap(deferred.value)(a0 => cont(a0).value.run)
override def map[B](f: A => B)(implicit F: Functor[F]) =
new Computation(deferred, (a0: A0) => Eval.defer(cont(a0)).map(_.map(f)))
override def flatMap[B](f: A => Sealed[F, B, ADT]) =
new Computation(deferred, (a0: A0) => Eval.defer(cont(a0)).map(_.flatMap(f)))
}
We just created Shlemiel the Painter algorithm :-(
Let's call it "Sealed"
sealed abstract class Sealed[F[_], +A, ADT] {
def run(implicit ev: A <:< ADT, F: FlatMap[F]): F[ADT] =
F.fmap(F.tailRecM(this)(_.step))(_.map(ev).merge)
def step[A1 >: A](implicit F: Functor[F]): F[Either[Sealed[F, A1, ADT], Either[ADT, A1]]]
def map[B](f: A => B): Sealed[F, B, ADT]
def flatMap[B](f: A => Sealed[F, B, ADT]): Sealed[F, B, ADT]
}
We have to be smarter ...
Let's call it "Sealed"
final class Computation[F[_], A0, A, ADT](fa0: Eval[F[A0]],
cont: A0 => Eval[Sealed[F, A, ADT]])
extends Sealed[F, A, ADT] {
override def step[A1 >: A](implicit F: Functor[F]) =
F.fmap(fa0.value)(a0 => Left(cont(a0).value))
override def map[B](f: A => B)(implicit F: Functor[F]) =
new Computation(fa0, (a0: A0) => Eval.defer(cont(a0)).map(_.map(f)))
override def flatMap[B](f: A => Sealed[F, B, ADT]) =
new Computation(fa0, (a0: A0) => Eval.defer(cont(a0)).map(_.flatMap(f)))
}
Yay! Works!
Let's call it "Sealed"
sealed abstract class Sealed[F[_], +A, ADT] {
// ...
def semiflatMap[B](f: A => F[B]) = flatMap(a => new Value(Eval.later(f(a))))
def complete[B](f: A => F[ADT]) = flatMap(a => new Result(f(a)))
def attempt[B](f: A => Either[ADT, B])(implicit F: Monad[F]) = map(f).rethrow
def attemptF[B](f: A => F[Either[ADT, B]])(implicit F: Monad[F]) = semiflatMap(f).rethrow
def rethrow[B](implicit ev: A <:< Either[ADT, B], F: Monad[F]) = flatMap { a =>
ev(a) match {
case Right(b) => new Value(Eval.later(F.pure(b)))
case Left(adt) => new Result(F.pure(adt))
}
}
def ensure(pred: A => Boolean, orElse: => ADT)(implicit F: Monad[F]) =
ensureOr(pred, _ => orElse)
def ensureOr(pred: A => Boolean, orElse: A => ADT)(implicit F: Monad[F]) =
attempt(a => Either.cond(pred(a), a, orElse(a)))
}
Add some useful combinators
Let's call it "Sealed"
object Sealed {
def liftF[F[_], ADT] = new LiftFPartiallyApplied[F, ADT]
def value[ADT] = new ValuePartiallyApplied[ADT]
def valueOr[F[_]: Monad, A, ADT](fa: F[Option[A]], orElse: => ADT) =
value[ADT](fa).attempt(Either.fromOption(_, orElse))
def valueOrF[F[_]: Monad, A, ADT](fa: F[Option[A]], orElse: => F[ADT]) =
value[ADT](fa).flatMap {
case Some(a) => liftF[F, ADT](a)
case None => resultF(orElse)
}
def merge[F[_]: Monad, A, B, ADT](fa: F[Either[A, B]])(f: Either[A, B] => ADT) =
mergeF(fa)(either => Monad[F].pure(f(either)))
def mergeF[F[_], A, B, ADT](fa: F[Either[A, B]])(f: Either[A, B] => F[ADT]) =
value[ADT](fa).complete(f)
def handleError[F[_]: Monad, A, B, ADT](fa: F[Either[A, B]])(f: A => ADT) =
value[ADT](fa).attempt(_.leftMap(f))
//...
}
Add some useful constructors
Let's call it "Sealed"
- Add syntax
- Add laws
- Test monad laws (it is a monad as long as F is one)
def resultMapElimination[A, B](fb: F[B], f: A => B) =
Sealed.resultF(fb).map(f) <-> Sealed.resultF(fb)
def resultFlatMapElimination[A, B](fb: F[B], f: A => Sealed[F, A, B]) =
Sealed.resultF(fb).flatMap(f) <-> Sealed.resultF(fb)
// ...
def resultSemiflatMapElimination[A, B](fb: F[B], f: A => F[B]) =
Sealed.resultF(fb).semiflatMap(f) <-> Sealed.resultF(fb)
def valueCompleteIdentity[A, B](fa: F[A], f: A => F[B]) =
Sealed.value[B](fa).complete(f) <-> Sealed.resultF(fa >>= f)
def rethrowRightIdentity[A, B](s: Sealed[F, A, B]) = s.map(Right(_)).rethrow <-> s
// ...
def ensureRethrowCoherence[A, B, C](s: Sealed[F, A, C], f: A => Boolean, c: C) =
s.ensure(f, c) <-> s.map(a => Either.cond(f(a), a, c)).rethrow
... and finally ...
Let's call it "Sealed"
userRepository.find(email).flatMap {
case None => M.pure(LoginResponse.InvalidCredentials)
case Some(user) if user.archivedAt.isDefined => M.pure(LoginResponse.Deleted)
case Some(user) =>
val authMethod = authMethodFromUserIdF(user.id)
val actionT = OptionT(authMethodRepository.find(user.id, authMethod.provider))
.map(checkAuthMethodAction(_))
actionT.value flatMap {
case Some(true) => M.pure(LoginResponse.LoggedIn(issueTokenFor(user)))
case Some(false) => M.pure(LoginResponse.InvalidCredentials)
case None => authMethod.mailToken match {
case Some(token) =>
sendConfirmationEmail(token, request.loginUrl, request.email)
.map(_ => LoginResponse.AccountsMergeRequested)
case None => M.pure(LoginResponse.InvalidCredentials)
}
}
}
val s = for {
user <- findUser(email)
.valueOr[LoginResponse](LoginResponse.InvalidCredentials)
.ensure(!_.archived, LoginResponse.Deleted)
userAuthMethod = authMethodFromUserIdF(user.id)
authMethod <- findAuthMethod(user.id, userAuthMethod.provider)
.valueOrF(mergeAccountsAction(userAuthMethod, user))}
yield if (checkAuthMethodAction(authMethod)) LoginResponse.LoggedIn(issueTokenFor(user))
else LoginResponse.InvalidCredentials
s.run
Let's call it "Sealed"
val s = for {
method <- findAuthMethod(token).valueOr[ConfirmResponse](ConfirmResponse.MethodNotFound)
user <- findUser(method.userId).valueOr[ConfirmResponse](ConfirmResponse.UserNotFound) !
upsertAuthMethod(confirmMethod(method))
} yield ConfirmResponse.Confirmed(issueTokenFor(user))
s.run
val userT = for {
method <- EitherT.fromOptionF(findAuthMethod(token),
ifNone = ConfirmResponse.MethodNotFound)
user <- EitherT.fromOptionF(findUser(method.userId),
ifNone = ConfirmResponse.UserNotFound: ConfirmResponse)
} yield (method, user)
userT.semiflatMap {
case (method, user) => upsertAuthMethod(confirmMethod(method))
.map(_ => ConfirmResponse.Confirmed(issueTokenFor(user)))
}.merge
Not so fast ...
It is slow. One liners are ~ 4x slower than their EitherT equivalents; longer programs are ~ 30% slower. What is the cause?
Digression: Absolutely everyone should use JMH! Use sbt-jmh in your build and install async-profiler for beautiful flame graphs. One-liner for your build.sbt:
addCommandAlias("flame", "benchmarks/jmh:run -p tokens=64 -prof jmh.extras.Async:dir=target/flamegraphs;flameGraphOpts=--width,1900")
Not so fast ...
Blackhole
Eval !!!
Final touch
case class Computation[F[_], A0, A, ADT](current: Sealed[F, A0, ADT],
cont: A0 => Sealed[F, A, ADT])
extends Sealed[F, A, ADT] {
override protected def step[A1 >: A](implicit F: Monad[F]) = current match {
case Result(result) => F.fmap(result)(adt => Right(Left(adt)))
case Value(fa0) => F.fmap(fa0)(a0 => Left(cont(a0)))
case Computation(prev, g) => F.pure(Left(prev.flatMap(a0 => g(a0).flatMap(cont))))
}
// ...
}
Crucial observation: Eval could be replaced with simple pattern match - tailRecM does all the hard work anyway. Performance is now ~ 5-10% worse than EitherT
That's all Folks!
iteratorshq.com
medium.com/iterators
slides from this presentation are available here: https://bit.ly/2TliUkI
Thanks for coming
Using monads to enforce programming style your Boss likes
By Marcin Rzeźnicki
Using monads to enforce programming style your Boss likes
- 1,411