Field Notes

Cats Core, Interlude: Reading fa, f and F[_]

Cats Core, Interlude: Reading fa, f and F[_]

In Part 4 I wrote a Functor instance, checked both laws and reused it in a generic function. Then I read it again and stalled on the parameter list:

implicit val quoteFunctor: Functor[Quote] = new Functor[Quote] {
  override def map[A, B](fa: Quote[A])(f: A => B): Quote[B] =
    Quote(f(fa.value), fa.source)
}

fa? f? Coming from Java, I expect a parameter name to say what the thing is: quote, transform, priceRule. These letters read like abbreviations I was supposed to know already.

This interlude adds no new type class. It is the decoding rule I wanted before Part 4: two rules that cover most Cats signatures, and the one name that breaks them.

Rule one: uppercase is a type, lowercase is a value

Here is the method as the Functor trait declares it in Cats Core 2.13.0:

def map[A, B](fa: F[A])(f: A => B): F[B]
  • A and B are placeholder types, such as BigDecimal and String.
  • F is the container type, such as List, Option or my Quote. The trait declares it as F[_]: a type that takes one type argument.
  • fa and f are the two values the caller passes in at runtime.

Every uppercase letter in the signature is a type. Every lowercase name is a value.

Rule two: a lowercase name spells its type

This is the part that made the letters readable for me. A value is named after its type, written in lowercase. fa has type F[A], so I read it as “an F of A”. In my instance it is a Quote[A]: a quote holding an A.

The same rule decodes names I had not stopped on yet. I checked each one against the Cats Core 2.13.0 sources jar rather than a cheat sheet. An earlier version of this table, written from memory, had two entries wrong: product takes fa and fb, not fab, and foldLeft calls its starting value b, not z. The z spelling is the Scala standard library’s.

Table of Cats parameter names: fa and fb are F[A] and F[B]; ff is F[A => B] in Apply.ap; fab is F[(A, B)] in Functor.unzip; fga is F[G[A]] in Traverse.sequence; b is a plain starting value in Foldable.foldLeft; f and g are functions, the exception.
Each name is its type in lowercase. Only the function names f and g break the pattern.

With the pattern in place, an unfamiliar signature reads as a sentence:

def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

“Give me a function inside an F and a value inside an F, and I return the result inside an F.” Apply, from Part 5, builds its product on this method by default. With Option as the F:

val ff: Option[Int => Int] = Some(_ + 1)

Apply[Option].ap(ff)(Some(2))
// Some(3)

The exception: f is the function

One name does not follow rule two. Lowercase f does not mean “an F”. It is the function, and when a signature needs a second function, that one is g. Invariant.imap takes both: f: A => B one way and g: B => A back.

The convention comes from Haskell, where the same letter has the other job. The Functor class in Haskell’s base declares:

fmap :: (a -> b) -> f a -> f b

Haskell writes type variables in lowercase, so here f is the container and f a is what Scala writes as F[A]. Scala writes type parameters in uppercase, so the container became F, which left lowercase f free to name the function. If you read Haskell material next to the Cats documentation, the letter switches meaning between the two.

Side by side: Haskell fmap :: (a -> b) -> f a -> f b, where f is the container; Cats def map[A, B](fa: F[A])(f: A => B): F[B], where f is the function.
In Haskell, f is the container. In Cats it is the function, because the container moved to uppercase F.

The Haskell instance for the same Quote also shows why Haskell never needs a name like fa:

data Quote a = Quote { value :: a, source :: String }

instance Functor Quote where
  fmap f (Quote v src) = Quote (f v) src

The argument is taken apart where it is declared. Its parts get names, v and src, and the whole value never needs one.

In my own instance, the names are mine

In the trait, the generic letters are honest. Functor knows nothing about its F except its shape, so there is no better name to give it. My instance does know it is dealing with quotes, and Scala lets an override rename its parameters. This version behaves exactly the same:

implicit val quoteFunctor: Functor[Quote] = new Functor[Quote] {
  override def map[A, B](quote: Quote[A])(transform: A => B): Quote[B] =
    Quote(transform(quote.value), quote.source)
}

Two other spellings keep the Cats names in the signature, so it still matches the documentation and the IDE hints, and name the parts in the body instead. Pattern matching is the closest to the Haskell instance:

override def map[A, B](fa: Quote[A])(f: A => B): Quote[B] =
  fa match {
    case Quote(value, source) => Quote(f(value), source)
  }

And copy states the Functor rule directly: replace the value and keep everything else.

override def map[A, B](fa: Quote[A])(f: A => B): Quote[B] =
  fa.copy(value = f(fa.value))

The copy version compiles even though the type changes from Quote[A] to Quote[B], because the copy method generated for a generic case class has its own type parameter. It also keeps working if Quote gains a field later: the new field is carried across without an edit to map.

I compiled all four instances with Scala 2.13.18 and Cats Core 2.13.0, then mapped the same Quote(BigDecimal(12), "catalog") through each with a label function. All four returned Quote(EUR 12,catalog). I did not compile the Haskell lines for this post; they are there for comparison.

For a small instance like this one, I would keep the Cats names in the signature and let the body name the parts. While the letters are still unfamiliar, the renamed version is a fair stepping stone.

Three checks before reading the implementation

  • An uppercase letter is a type; a lowercase name is a value.
  • A lowercase value name spells its type: fa is an F of A, fga is an F of G of A.
  • f and g are the exception: they are functions.

Reading a new signature aloud with those three checks, before opening its implementation, turns most of the alphabet soup back into a sentence.

References: Typelevel’s Functor documentation and the Data.Functor documentation in Haskell’s base.