Skip to content

Instantly share code, notes, and snippets.

@conniec
Last active March 12, 2019 00:16
Show Gist options
  • Save conniec/050435e50d3e75d897153c37503c4645 to your computer and use it in GitHub Desktop.
Save conniec/050435e50d3e75d897153c37503c4645 to your computer and use it in GitHub Desktop.
Chapter 6 — Applicative and Traversable Functors

Overview

In previous chapters, we see how functors and monads allow us to sequence operations in a program using map and flatMap. Applicative functors are a related abstraction that is less powerful than monads, and are more common.

Methods that define a Functor:

def map[A,B](a: F[A])(f: A => B): F[B]

Methods that define a Monad:

trait Monad[F[_]] extends Functor[F] {
   def unit[A](a: => A): F[A]
   def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]
}

And the methods below can be derived from the above methods for Monad's. And since we can implement a map here, we can also extend the Functor trait. (All monads are functors)

def map[A,B](ma: F[A])(f: A => B): F[B] = 
   flatMap(ma)(a => unit(f(a)))
   
def map2[A,B,C](ma: F[A], mb: F[B])(f: (A, B) => C): F[C] = 
   flatMap(ma)(a => map(mb)(b => f(a, b)))

Applicative trait

trait Applicative[F[_]] extends Functor[F] {
   def map2[A,B,C](fa: F[A], fb: F[B])(f: (A, B) => C): F[C]
   def unit[A](a: => A): F[A]

We can also use those primitive types to derive map, so Applicative's are also Functors.

def map[B](fa: F[A])(f: A => B): F[B] =
   map2(fa, unit(()))((a, _) => f(a))

All monads are Applicative's, because we can also implement map2 in terms of flatMap.

Applicative interface

The name applicative comes from the fact that we can formulate the Applicative interface using an alternate set of primitives, unit and the function apply, rather than unit and map2. (Exercise 12.2)

trait Applicative[F[_]] extends Functor[F] {
   def apply[A,B](fab: F[A => B])(fa: F[A]): F[B]
   def unit[A](a: => A): F[A]

Difference between Applicative and Monad computations

  • Applicative computations have fixed structure and simply sequence effects, whereas monadic computations may choose structure dynamically, based on the result of previous effects.
  • Applicative constructs context-free computations, while Monad allows for context sensitivity.
case class Row(date: Date, temperature: Double)

val F: Applicative[Parser] = ... val d: Parser[Date] = ...
val temp: Parser[Double] = ...
val row: Parser[Row] = F.map2(d, temp)(Row(_, _)) val rows: Parser[List[Row]] = row.sep("\n")

val F: Monad[Parser] = ...
val d: Parser[Date] = ...
val temp: Parser[Double] = ...
val header: Parser[Parser[Row]] = ... val rows: Parser[List[Row]] =
  F.flatMap (header) { row => row.sep("\n") }
# This flatMap depends on the result of the row and the previous result

Monad composition

Applicative instances compose, but Monad instances do not.

Because Applicative is “weaker” than Monad, this gives the interpreter of applicative effects more flexibility. To take just one example, consider parsing. If we describe a parser without resorting to flatMap, this implies that the structure of our grammar is determined before we begin parsing. Therefore, our inter- preter or runner of parsers has more information about what it’ll be doing up front and is free to make additional assumptions and possibly use a more effi- cient implementation strategy for running the parser, based on this known structure. Adding flatMap is powerful, but it means we’re generating our parsers dynamically, so the interpreter may be more limited in what it can do.

import cats.Monad
import cats.syntax.applicative._ // for pure
import cats.syntax.flatMap._     // for flatMap
import scala.language.higherKinds

// Hypothetical example. This won't actually compile:
def compose[M1[_]: Monad, M2[_]: Monad] = {
  type Composed[A] = M1[M2[A]]

  new Monad[Composed] {
    def pure[A](a: A): Composed[A] =
      a.pure[M2].pure[M1]

    def flatMap[A, B](fa: Composed[A])
        (f: A => Composed[B]): Composed[B] =
      // Problem! How do we write flatMap?
      ???
  }
}

If we fix the Monad to Option, we can write flatMap like this (notice how we use None, which is Option-specific)

def flatMap[A, B](fa: Composed[A])
    (f: A => Composed[B]): Composed[B] =
  fa.flatMap(_.fold(None.pure[M])(f))

Validated

Variant of Either that accumulates errors.

Traversable

We discovered applicative functors by noticing that our traverse and sequence functions (and several other operations) didn’t depend directly on flatMap.

trait Traverse[F[_]] {
   def traverse[G[_]:Applicative,A,B](fa: F[A])(f: A => G[B]): G[F[B]] =
      sequence(map(fa)(f))
   def sequence[G[_]:Applicative,A](fga: F[G[A]]): G[F[A]] =
      traverse(fga)(ga => ga)
}

List[Option[A]] => Option[List[A]] (a call to Traverse[List].sequence with Option as the Applicative) returns None if any of the input List is None; otherwise it returns the original List wrapped in Some.

Monad Transformers

case class OptionT[M[_],A](value: M[Option[A]])(implicit M: Monad[M]) { 
   def flatMap[B](f: A => OptionT[M, B]): OptionT[M, B] =
      OptionT(value flatMap {
         case None => M.unit(None) 
         case Some(a) => f(a).value
    }) 
}
OptionT[List, A]
// Is the same as List[Option[A]]

EitherT[Option, A, B]
// Is the same as Option[Either[A, B]]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment