#fp

April 26, 2019

Applicative Functors - "web-scale" edition

TLDR: Applicative functors are great! Here’s an example that isn’t about containers or collections.

At work we have multiple services and serverless functions (hence web-scale) that require secrets: configuration values that are needed at runtime but are out of source control. We standardized on storing secrets in the Parameter Store (PS), and read them in different ways according to the situation:

  • for AWS Lambda functions we make an call to the PS directly
  • for dockerized services we use chamber to inject secrets into the process environment
  • for some local tests, developers can use chamber to export to a temporary file

For example: to use some-api.com we might need read SOME_API_TOKEN from the PS, the environment, and from a line in a local file.

Applicative functors allow combining computations, which, in our case, corresponds to combining multiple secrets into a value we can use.

Here’s our Secret type and its Applicative 1 instance:

// provide a value given a key
trait Get extends (String => Option[String])

// given a `Get`, produce an A or fail with an error message
trait Secret[A] {
  def load(get: Get): Either[String, A]
}

import cats.Applicative
import cats.implicits._

object Secret {

  // the simplest secret is a single string
  def string(key: String): Secret[String] = get =>
    get(key).toRight(s"Missing key: $key")

  // the instance to enable applicative operations and syntax, provided by cats
  implicit val secretApplicative = new Applicative[Secret] {
    def pure[A](x: A): Secret[A] = _ => Right(x)

    def ap[A, B](ff: Secret[A => B])(fa: Secret[A]): Secret[B] = get =>
      for {
        a <- fa.load(get)
        f <- ff.load(get)
      } yield f(a)
  }
}

And this is how we’d use it in an application:

import Secret._

object SomeApi {
  case class Token(raw: String)
}

object SomeDb {
  case class Credentials(user: String, pw: String)
}

object Secrets {
  case class All(api: SomeApi.Token, db: SomeDb.Credentials)

  val someApiToken: Secret[SomeApi.Token] =
    string("SOME_API_TOKEN").map(SomeApi.Token)
  val someDb: Secret[SomeDb.Credentials] = (
    string("SOME_DB_USER"),
    string("SOME_DB_PW")
  ).mapN(SomeDb.Credentials)

  val all: Secret[All] = (someApiToken, someDb).mapN(All)
}

Notice how we did not have to write mapN (which is defined up to 22-tuples!) or map, cats can provide them (and many more), based solely on our secretApplicative.

Why go to all this trouble? Plain lazyness pragmatism: it’s annoying to implement mapN up to whatever number of arguments are required. More importantly, once the concept of applicative functors is internalized, there’s almost no choice but to notice the pattern and reach for a library (in the scala case) to exploit it.

Update:

@yuvalitzchakov pointed out on twitter that Get would be more useful if the signature was String => F[Option[String]]. This is true because it might be useful to do a side-effect, but in our particular use-case at work it’s not required and it wouldn’t make this post clearer.



  1. This example uses Cats although Scalaz has exactly the same concept, possibly with some differences in nomenclature [return]