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]AandBare placeholder types, such asBigDecimalandString.Fis the container type, such asList,Optionor myQuote. The trait declares it asF[_]: a type that takes one type argument.faandfare 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.](/blog/media/5e57b7f8139abcb376bb2b68a90f0efe29c31ad9b8d228c96fa72b9a650413cb.png)
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 bHaskell 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.
(f: A => B): F[B], where f is the function.](/blog/media/8ab81aa4311473417694bac247ecd8159522583e33601c854605ad41427ac2bf.png)
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) srcThe 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:
fais an F of A,fgais an F of G of A. fandgare 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.