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":
But EitherT is nice. Look ...
What's not to like?
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:
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?
What's not to like, boss?
What's not to like?
// 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?
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?
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:
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:
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"
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
iteratorshq.com
medium.com/iterators
slides from this presentation are available here: https://bit.ly/2TliUkI
Thanks for coming