Kittens High on Catnip -
Squeaky Clean Type Class Derivation in Scala

Kiril Yurovnik
Riskified Tech
Published in
6 min readJan 13, 2021

--

Type classes are a very powerful software design pattern and an integral part of the Scala language. Popular libraries, such as Cats, provide some extremely useful general-purpose type class definitions, which come in handy in various use cases.

At Riskified, we often ask ourselves whether a new piece of code can be implemented based on these general type classes. This commonly results in robust, clean and idiomatic solutions.
To answer that question, I usually open a Scala REPL and start trying things out (a.k.a prototyping). However, defining the specific type class instances (which are often very trivial) can be pretty tedious and make prototyping less efficient.
Fortunately, some libraries provide automatic type class derivation, while others provide neat syntax that helps keep code clean and simple when using it.

In this post, we will start by examining how you can use auto derivation for faster and more efficient prototyping. Then, we’ll discuss the caveats when moving to production code, and how you can avoid them. I will close by showing how you can make your auto-deriving production code even tidier and cleaner.

Prototyping with type classes: what could go wrong?

So let’s say you are working on a shopping application, specifically the part which manages shopping carts. A basic cart can be described by the following case class:

Where CatalogItem and Usd are just value classes for String and Double, respectively.

Now let’s assume that a user can create several carts, and be able to combine them together, for example:

ShoppingCart(List(CatalogItem(“Shirt”)), Usd(20)) combine
ShoppingCart(List(CatalogItem(“Shoes”)), Usd(50))

will result in:

ShoppingCart(List(CatalogItem(“Shirt”), CatalogItem(“Shoes”)), Usd(70))

Of course you could easily implement the requirement for this simple example, but it wouldn’t scale well for more complex real-world use cases, in addition to being less clean and idiomatic. So let’s ask ourselves whether a general-purpose type class can be used instead. Hint: Yes, it can!
As a matter of fact, this is exactly what Cats’ Semigroup[A] type class represents: it defines a single method named “combine” that enables, well, combining, instances of the type parameter A.

So instead of implementing this on your own, you could define a Semigroup[ShoppingCart] instance. Notice that this “combine” operation is recursive in a sense — combining shopping carts requires combining items lists and Usd prices, which in turn requires combining (i.e. summing) Double values.
Fortunately, Cats provides standard Semigroup instances for List (with concatenation) and Double (with addition) out-of-the-box, using a simple import, so you’ll only need to provide the following instances: (note the “|+|” operator syntax, which is a shortcut for Semigroup’s “combine”):

Now, you may already have an idea of why I said that this quickly becomes tedious and inefficient — these Semigroup instances are very trivial, and there are just two. Imagine a real-world application where there could be dozens or more! So just using a general-purpose type class might make your code a bit more idiomatic, but it won’t necessarily make your code cleaner or your life easier. Let’s see how we can get around writing all that repetitive and trivial code and benefit from using type classes to the fullest.

Increase prototyping speed with fully automatic derivation

This is where kittens comes into play.

Fun fact: the module structure of kittens does not include the word “kittens”, as opposed to most other libraries. Instead, it starts with “cats.derived”. Kittens are “derived” from cats, if you think about it ;)

Kittens will automatically create type class instances for your ADTs — all you have to do is add an import statement.

Going back to our shopping cart example, we can replace all the Semigroup instances (of which there can be dozens, as you recall) with a single import that will tell kittens to create them automatically, when needed. This reduces the code to simply:

You might wonder — how does kittens “know” how these Semigroup instances should be implemented? It’s a fair question:
Recall what we said about those instances being trivial. Not only because they are short and simple, but because they essentially do the exact same thing — going over the original type’s fields and combining them respectively. This is exactly what kittens does “under the hood”— it performs a “structural” derivation based on product members and sealed inheritance. In other words, it will derive an instance for each field’s type (in the shopping cart example -List[CatalogItem] and Usd) and then create an instance for the original type that combines the pair of values for each field.

Note that this means that if your desired type class implementation differs from this generic behavior, you won’t be able to derive it automatically and will have to implement it yourself.

Production-grade automatic type class derivation

So, by now you have a fully-functional prototype of your desired behavior, and you’ve made it work in record time using kittens’ automatic type class derivation. Now it’s time to integrate your prototype code into the production codebase.

The simplest way to do that is by copying the prototype code, along with the cats.derived.auto.x._ imports, and you’re done — fast, simple and clean. But there’s a catch — there are several “modes” of automatic derivation, and the full-auto mode (the one we discussed so far) will re-derive for every use site (even if an identical instance was already derived in another place). This can have a very significant impact on the project’s compilation time.

The recommended best practice is to use semi-auto derivation instead, which would look like this:

This fixes the issue of repeated re-derivation, by manually caching the type class instance in the implicit val.

This kind of derivation can be used generically for different types by pretty much copy-pasting this code snippet — after all, this is the whole idea behind automatic derivation.

It’s worth mentioning kittens’ two additional modes of derivation: “full-auto cached” and “manual semi”. Keep in mind that although they look cleaner and simpler than the recommended “semi-auto” mode, they have pitfalls that might cause issues in certain cases (more details on the kittens Github page).

So let’s stick with the recommended semi-auto derivation mode. You might argue that it still involves too much boilerplate, and you would be absolutely right. Let’s see what we can do about that.

Getting rid of the excess boilerplate

So you’ve migrated your prototype code into the production codebase and copy-pasted some semi-auto derivation snippets — everything works like a charm and compilation time hasn’t increased significantly.
But you’re a perfectionist, like me, and this semi-auto derivation code just hurts your eyes every time you look at it.
Worry not! Drugs to the rescue (if you’re a kitten, that is)!

Catnip addresses exactly this problem. It provides class annotations as an alternative to writing kittens semi-auto derivation code by hand. The annotations are very simple and clean (@Semi for “semi-auto”):

Also, even though catnip only provides out-of-the-box annotations for the type classes supported by kittens, it is possible to extend its functionality by providing custom type class generator mappings.

It’s also worth mentioning that while kittens and catnip provide neat auto derivation for the type classes in Cats, there are other libraries that provide similar functionality for other type classes. For example, scalaz-deriving provides convenient auto-derivation for scalaz type classes, also with the ability to extend its functionality for additional type classes (even your own!).

Looking forward, Scala 3 is expected to support type class derivation natively, with a pretty neat syntax of its own:

Wrapping up

Hopefully, this post provided you with the basis and tools for being able to start experimenting and prototyping with general-purpose type classes, and later on integrating them into your production code in a correct, simple and clean way.

In this post we mainly referred to Cats’ Semigroup type class. However, Cats, and other libraries, contain many other useful type class definitions. I highly recommend getting familiar with them and using them in your daily work.

A project complete with all examples above is available at https://github.com/kirilyuro/kittens-high-on-catnip.

--

--