Search code examples
scalafunctional-programmingscala-catsapplicativeflatmap

How to flatMap cats Applicatives


I've started learning functional programming with Cats and I stuck with flatMapping (merging) applicatives F[List].

What is very simple in pure Scala is flatmapping list of lists like that:

val animals = List("Dog", "Cat", "Bird")
def getBreads(animal: String): List[String] = ...

val allAnimalsBreads = animals.flatMap(animal => getBread(animal)) // this will be just List[String]

How can I do the same if everything is wrapped with applicative?:

val animals = List("Dog", "Cat", "Bird").pure[F]
def getBreads(animal: String): F[List[String]] = ...

val allAnimalsBreads = ? // this should be F[List[String]]

Solution

  • Applicative provides ap and pure, but does not guarantee to provide flatMap, which is provided by Monad:

    Monad extends the Applicative type class with a new function flatten.

    If F was a monad, then at least in scalaz we might use ListT, for example,

    import scalaz._
    import ListT._
    import scalaz.std.option._
    
    val animals: Option[List[String]] = Some(List("Dog", "Cat", "Bird"))
    def getBreeds(animal: String): Option[List[String]] = ???
    
    (for {
      animal <- listT(animals)
      breed <- listT(getBreeds(animal))
    } yield breed).run
    

    However cats does not seem to provide ListT:

    A naive implementation of ListT suffers from associativity issues; ... It’s possible to create a ListT that doesn’t have these issues, but it tends to be pretty inefficient. For many use-cases, Nested can be used to achieve the desired results.


    Here is an attempt at a mad solution that you should not use. Consider Validated which only has Applicative instance. Let us provide a Monad instance even though Validated is not a Monad:

    implicit def validatedMonad[E]: Monad[Validated[E, *]] =
      new Monad[Validated[E, *]] {
        def flatMap[A, B](fa: Validated[E, A])(f: A => Validated[E, B]): Validated[E, B] =
          fa match {
            case Valid(a) => f(a)
            case i @ Invalid(_) => i
          }
    
        def pure[A](x: A): Validated[E, A] = Valid(x)
    
        def tailRecM[A, B](a: A)(f: A => Validated[E, Either[A, B]]) = ???
      }
    

    The implementation of validatedMonad is taken from scala-exercises.org/cats/validated.

    Next let us make scalaz's listT available within cats via shims interop layer

    libraryDependencies += "com.codecommit" %% "shims" % "2.1.0"
    

    Putting it all together, we have

    import cats._
    import cats.Monad
    import cats.data.Validated.{Invalid, Valid}
    import cats.data.{Nested, OptionT, Validated, ValidatedNec}
    import cats.implicits._
    import scalaz.ListT._
    import shims._
    
    implicit def validatedMonad[E]: Monad[Validated[E, *]] =
      new Monad[Validated[E, *]] {
        def flatMap[A, B](fa: Validated[E, A])(f: A => Validated[E, B]): Validated[E, B] =
          fa match {
            case Valid(a) => f(a)
            case i @ Invalid(_) => i
          }
    
        def pure[A](x: A): Validated[E, A] = Valid(x)
    
        def tailRecM[A, B](a: A)(f: A => Validated[E, Either[A, B]]) = ???
      }
    
    val animals: Validated[String, List[String]] = List("Dog", "Cat", "Bird").valid
    def getBreeds(animal: String): Validated[String, List[String]] = ???
    
    (for {
      animal <- listT(animals)
      breed <- listT(getBreeds(animal))
    } yield breed).run
    

    Note this "solution" breaks monadic laws, is not general, and is likely to cause confusion, so do not use it.