Skip to content

Proposal: MonadError.transformWith #2161

@alexandru

Description

@alexandru

So MonadError has attempt and handleErrorWith and for many use-cases one has to introduce both map / flatMap transformations and handleErrorWith handlers in the same "frame".

E.g. what happens for data types implementing cats-effect like cats.effect.IO and monix.eval.Task is something like this

fa.attempt.flatMap {
  case Right(next) => continueLoop(next)
  case Left(error) => stopLoop(error)
}

This is more or less the equivalent of this pattern:

var streamError = true
try {
  val next = fa()
  streamError = false
  continueLoop(next)
} catch {
  case error if streamError => 
    stopLoop(error)
}

Well the problem is that usage of either attempt or handleErrorWith involves 2 flatMap (bind) frames instead of one. And as we all know, the performance of IO / Task data types, compared with plain function calls, is terrible.

The proposal is to introduce an alternative to attempt.flatMap like so:

def transformWith[A, B](fa: F[A])(bind: A => F[B], recover: Throwable => F[B]): F[B]

Just did.a fast benchmark to show what the difference can be:

attempt             10000  thrpt   10  2383.878 ±  46.546  ops/s
onErrorHandleWith   10000  thrpt   10  2813.948 ± 176.036  ops/s
transformWith       10000  thrpt   10  4115.213 ±  40.821  ops/s

Precedents:

  1. Task.transformWith
  2. Future.transformWith

Note — I'd prefer the version with the two function parameters, because I'd rather avoid the extra Either boxing.

Implementation notes:

  1. cats.effect.IO is already optimized for this operation
  2. monix.eval.Task is already optimized for this operation

Basically I'd like to not feel bad about adding error handling.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions