Home > Enterprise >  Flattening mixed nested monads in cats (composing complex Kleislis)
Flattening mixed nested monads in cats (composing complex Kleislis)

Time:01-10

I would like to understand how I should compose Kleislis that return "complex" types (e.g. F[Either[A,B]]). Here's an example of what I mean:

This is a simple case - given two Kleislis

val parseRegistration: Kleisli[Result, Auth.Registration, Auth.Registration] = ???
val hashPassword: Kleisli[Result, Auth.Registration, Auth.Registration] = ???

I can compose them in this way:

val validateAndHash = parseRegistration andThen hashPassword

And all is fine.

However, given more complex Kleisli types (where the return value is an Either[A, B]), I'm not able to compose them directly (because I ended up with F[Either[A,B]] where a Kleisli expects only F[A]).

// relevant type definitions
final case class ServiceError(kind: ErrorKind, message: String = "", cause: Option[Throwable] = None)
final type Result[A] = Either[ServiceError, A]
final case class Registration(email: String, password: String)
final case class Registered(session: Session)


val createUser: Kleisli[F, Auth.Registration, Result[Long]] = ???
val createSession: Kleisli[F, Long, Result[Session]] = ???


// does not compile (no overloaded alternatives match the arguments) - I expected this as  
// createUser returns F[Result[Long]] and the createSession Kleisli expects only F[Long]
val createUserAndSession = createUser andThen createSession

Ok, so as expected. Then, instead of composing them, I tried simply performing nested maps:

val r1 = validateAndHash(info) // Result[Auth.Registration]
val r2 = r1.map(i => createUser(i)) // Either[ServiceError, F[Result[Long]]]
val r3 = r2.map(_.map(_.map(uid => createSession(uid).map(_.map(Auth.Registered.apply)))))
// Now I've got an Either[ServiceError, F[Either[ServiceError, F[Either[ServiceError, Auth.Registered]]]]]
// in r3! I wanted to end up with F[Result[Auth.Registered]]

So I've ended up causing increasing nesting and flatten doesn't seem to help because F and Result can't be flattened into each other.

I'm sure this is something the seasoned scala cats programmers encounter all the time, so I'd appreciate it if you could help me understand how I can avoid getting myself into this mess?

EDIT:

I have now defined a function to invert the nested wrapper types so they can then be flattened:

  def invert[S, F[_]: Monad, V](e: Either[S, F[V]]): F[Either[S, V]] =
    e match {
      case Left(s) => Monad[F].pure(Left(s))
      case Right(fv) => fv.map(Right(_))
    }

This allows me to do this:

val r1 = validateAndHash(info)
val r2 = invert(r1.map(i => createUser(i))).map(_.flatten)
val r3 = r2.map( i => invert(i.map(uid => createSession(uid).map(_.map(Auth.Registered.apply)))).map(_.flatten))
val r4 = Monad[F].flatten(r3)

Which seems... clumsy? And too specific to Either. Is there a cleaner way to do this using something built in to cats?

CodePudding user response:

I would rather compose values than play with Kleisli, especially since EitherT makes this pretty simple.

First, let's refactor createUser & createSession to be normal methods:

def createUser[F[_]](registration: Registration): F[Result[Long]] = ???
def createSession[F[_]](long: Long): F[Result[Session]] = ???

And now let's implement createUserAndSession

import cats.Monad
import cats.data.EitherT

def createUserAndSession[F[_]](registration: Registration)(implicit ev: Monad[F]): F[Result[Session]] =
  EitherT(createUser(registration)).flatMapF(createSession).value
  •  Tags:  
  • Related