Cats is built on type classes. Before you can compare two values with ===, print them with show, sort them with Order, merge them with |+| or traverse a structure with traverse, the compiler needs an instance of the matching type class for your type. For library types Cats ships the instances. For your own case classes and enums you have to supply them, and writing them by hand is repetitive: an Eq for a ten-field record is ten comparisons joined with AND, a Monoid is ten field-wise combines, and every new field is a chance to forget one.
Kittens is the Typelevel library that writes those instances for you. It reads the shape of a type at compile time, as a product of fields or a sum of cases, and builds the instance from the instances of its parts. This article explains how that works, how to use it on Scala 3 and Scala 2, which mode to pick, how to derive higher-kinded classes such as Functor and Traverse, how to prove the derived instances are lawful, and where derivation goes wrong. For the theory of generic representations underneath, see Shapeless; for how Scala 3 finds the instances once they exist, see Scala 3 givens.
Why derive instead of hand-writing
A hand-written instance is correct on the day you write it. The problem is the day after. Someone adds a currency field to Money, the compiler is satisfied because the hand-written Eq still compiles, and two amounts in different currencies now compare equal. Derived instances do not drift: they are regenerated from the type on every compile, so a new field is automatically compared, printed, hashed and combined.
Derivation also removes a class of inconsistency between instances. A hand-written Hash that ignores a field the hand-written Eq checks breaks the rule that equal values must have equal hashes, and hash maps keyed on the type start losing entries. Kittens derives both from the same field list, so they agree by construction.
The cost is control. A derived instance does what the structure says, not what the domain means. If two Email values should compare case-insensitively, or a Show must mask a card number, the derived instance is wrong and you write that one by hand. Derive what is structural; hand-write what is a business decision.
How derivation works
Every case class is a product: a fixed list of labelled fields. Every sealed trait or Scala 3 enum is a sum: exactly one of a fixed list of cases. Most type classes have an obvious rule for each. Equality of a product is equality of every field; equality of a sum is same case and then equality within that case. Show of a product prints the type name and each labelled field. A Monoid of a product combines field by field, and its empty value is the tuple of each field's empty value; sums have no sensible Monoid, so Kittens does not offer one for them.
To apply those rules the library needs to see the fields at compile time. On Scala 3 the compiler provides that view as a Mirror, and Kittens uses the shapeless-3 derivation toolkit on top of it. On Scala 2 it uses shapeless 2's Generic and LabelledGeneric. In both cases the output is ordinary instance code, resolved and type-checked by the compiler, with no reflection at runtime.
The diagram shows the step that matters in practice: for each field, the derivation needs an instance of the same type class. For List[Line] Cats already provides Eq[List[A]] given an Eq[Line], so the question moves to Line. Whether Kittens may derive that nested instance on its own, or must find one already declared, is the difference between the default and strict modes described below.
Setting it up: derives, semiauto and auto
Add the dependency next to cats-core. The Scala 3 derives clause is the recommended style: it puts the instance in the type's companion, where implicit search always looks, so no import is needed at use sites.
// build.sbt
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.13.0",
"org.typelevel" %% "kittens" % "3.5.0" // latest release when written; check for newer
)
// Scala 3: the import is what makes `derives Eq` legal, because it adds a
// `derived` method for the cats companions that do not define one themselves.
import cats.*
import cats.derived.*
case class Money(cents: Long) derives Eq, Order, Show
case class Line(sku: String, qty: Int, price: Money) derives Eq, Show
case class Order(id: Long, lines: List[Line], total: Money) derives Eq, Show
enum Status derives Eq, Show:
case Pending, Paid, RefundedThe import cats.derived.* line is easy to miss and the error without it is confusing. A derives clause asks the compiler to call a derived method on the type class companion. Cats' own companions do not define one, so Kittens supplies it through that import. The type classes Kittens can derive on both Scala 2 and 3 include Eq, PartialOrder, Order, Hash, Show and a pretty-printing ShowPretty, Semigroup, Monoid and their commutative forms, SemigroupK, MonoidK, Functor, Contravariant, Invariant, Pure, Apply, Applicative, Foldable, Reducible, Traverse and NonEmptyTraverse. Scala 3 adds Alternative and NonEmptyAlternative among others.
Semiauto derivation is the explicit alternative, and the only style on Scala 2. You declare each instance yourself and let Kittens fill in the body:
// Scala 3 semiauto: explicit givens, often in the companion or in a separate "instances" object
import cats.*
import cats.derived.semiauto
object Order:
given Eq[Order] = semiauto.eq
given Show[Order] = semiauto.show
// Scala 2.13 semiauto: same idea, implicit vals
import cats.derived
object Order {
implicit val eqOrder: Eq[Order] = derived.semiauto.eq
implicit val showOrder: Show[Order] = derived.semiauto.show
}
// Auto derivation: instances appear wherever the import is in scope (use sparingly)
import cats.derived.auto.show.given // Scala 3
import derived.auto.show._ // Scala 2Auto derivation, by import, makes an instance appear for any type that fits wherever the import is visible. It is convenient in a REPL or a test, and risky in application code: the instance you get depends on which imports happen to be in scope, so the same expression can behave differently in two files, and compile times rise because the compiler attempts derivation at every search. Prefer derives or semiauto, which give every type exactly one instance, declared in one place.
Default mode versus strict mode
In the default mode, when a field's type has no instance, Kittens derives one for it too, recursively. That is convenient, and it hides decisions. If Line has a hand-written Show that masks the SKU, but it is declared in an object that is not imported where Order derives its instance, the default mode silently derives a structural Show[Line] inside Show[Order] and the masking is bypassed.
Strict mode, available through cats.derived.strict and strict.semiauto on Scala 3, does not derive nested instances. Every field must already have an instance in implicit scope, or compilation fails with a message naming the missing one. That is more typing, because each nested type needs its own derives clause, and it is the right trade for domain models where some instances are deliberately hand-written. A good policy is strict mode in core domain modules and the default mode in tests and throwaway code.
Deriving Functor, Foldable and Traverse
The most valuable instances to derive are the higher-kinded ones, because they are the most tedious and error-prone to write. A Traverse for a tree needs to visit every element in a consistent order and rebuild the structure inside an arbitrary applicative. Kittens derives it from an enum with one type parameter:
import cats.*
import cats.derived.*
import cats.syntax.all.*
enum Tree[+A] derives Functor, Foldable, Traverse:
case Leaf(value: A)
case Branch(left: Tree[A], right: Tree[A])
val t: Tree[String] = Tree.Branch(Tree.Leaf("12"), Tree.Branch(Tree.Leaf("7"), Tree.Leaf("x")))
t.map(_.length) // Branch(Leaf(2), Branch(Leaf(1), Leaf(1)))
t.foldMap(s => s.length) // 4
t.traverse(s => s.toIntOption) // None, because "x" does not parse
t.traverse(s => s.toIntOption.orElse(Some(0))) // Some(Branch(Leaf(12), Branch(Leaf(7), Leaf(0))))Derivation for type constructors follows the same product and sum rules, applied to positions where the type parameter appears. The derived instances visit fields in declaration order, so the left subtree is traversed before the right one. Order matters for effects: traversing with a validation or a state effect sees elements in that order.
The README documents three limits. Kittens cannot derive for nested type constructors such as [x] =>> List[Set[x]]. On Scala 3, derived instances are not stack safe, so a deeply recursive structure such as a ten-thousand-element linked list will overflow the stack in foldRight or traverse; hand-write those, or use the Cats collections. And a derives clause generates an instance that requires the type class for every type parameter, even a phantom one: case class Tagged[A](id: Long) derives Eq yields an instance that demands Eq[A] although no A is ever compared. Use semiauto with an explicit given for those types.
Worked example: a mergeable sales report
Suppose an order stream must be summarised per region, in parallel across partitions, with partial results merged at the end. The operation you need is a Monoid: an empty value and an associative combine. Deriving it for the summary types turns the whole aggregation into one foldMap:
import cats.*
import cats.derived.*
import cats.syntax.all.*
case class Totals(orders: Int, units: Long, revenueCents: Long) derives Monoid, Eq, Show
case class ByRegion(emea: Totals, amer: Totals, apac: Totals) derives Monoid, Eq, Show
def totalsFor(o: Order): Totals =
Totals(1, o.lines.map(_.qty.toLong).sum, o.total.cents)
def route(region: String, t: Totals): ByRegion = region match
case "EMEA" => ByRegion(t, Monoid[Totals].empty, Monoid[Totals].empty)
case "AMER" => ByRegion(Monoid[Totals].empty, t, Monoid[Totals].empty)
case _ => ByRegion(Monoid[Totals].empty, Monoid[Totals].empty, t)
// One pass, any partitioning: Monoid lets Spark, fs2 or a parallel fold combine partial results
case class Sale(region: String, order: Order)
val report: ByRegion = sales.foldMap(s => route(s.region, totalsFor(s.order)))Kittens derives Monoid[Totals] from Monoid[Int] and Monoid[Long], which Cats defines as addition with zero, and Monoid[ByRegion] from Monoid[Totals]. Because the combine is associative, the same code gives the same answer when partitions are merged in any grouping, which is exactly the property a streaming job or a Cats Effect parallel fold needs.
Two orders in EMEA, one with 3 units at 1,500 cents and one with 2 units at 900 cents, fold to Totals(2, 5, 2400) in the EMEA field and empty totals elsewhere. Add a refunds: Long field to Totals next quarter and the Monoid, Eq and Show all pick it up with no other change. That is the point of deriving.
Proving the instances are lawful
Every Cats type class comes with laws: Monoid combine must be associative with a neutral empty, Order must be total and transitive and agree with Eq, Traverse must satisfy identity and composition. Kittens follows the structural rules, so its instances are lawful when the field instances are. But one unlawful field instance, a Monoid that is not associative for example, makes the derived one unlawful too. The cats-laws module and discipline check the laws with generated inputs:
// test dependencies: cats-laws and discipline-munit (versions to match your cats)
import cats.kernel.laws.discipline.{MonoidTests, OrderTests}
import munit.DisciplineSuite
import org.scalacheck.{Arbitrary, Cogen, Gen}
class DerivedLawsSuite extends DisciplineSuite:
given Arbitrary[Totals] = Arbitrary(
for o <- Gen.choose(0, 1000); u <- Gen.choose(0L, 10000L); r <- Gen.choose(0L, 1000000L)
yield Totals(o, u, r))
given Arbitrary[Money] = Arbitrary(Gen.choose(-1000000L, 1000000L).map(Money(_)))
given Cogen[Money] = Cogen[Long].contramap(_.cents) // Order laws generate Money => Money
checkAll("Monoid[Totals]", MonoidTests[Totals].monoid)
checkAll("Order[Money]", OrderTests[Money].order)
// Traverse laws need Arbitrary[Tree[Int]] and friends; write one generator and reuse itLaw tests are cheap to write once you have generators, and they catch the problems derivation cannot see: a floating-point field that breaks associativity, a hand-written field instance that disagrees with its Eq, or a Traverse on a type that visits elements twice. The generators also serve property tests of your business logic, described in Scala testing.
Failure modes and trade-offs
- Show leaks data. A derived
Showprints every field, including tokens, emails and card numbers, and log lines built withshowinherit that. Hand-write Show for types that carry secrets and keep them in strict mode so the structural one cannot be derived by accident. - Order depends on field order. A derived
Ordercompares fields lexicographically in declaration order. Reordering fields in a refactor silently changes sort order, and so the output of anything sorted with it. Test the ordering of your key types explicitly. - Floating-point fields. Structural equality and combine on
Doubleinherit its quirks: NaN handling, and a Monoid that is not exactly associative because of rounding. Store money as integers orBigDecimal. - Recursive types. Scala 3 derived instances are not stack safe; deep structures overflow. Hand-write or bound the depth.
- Compile time. Derivation runs in the compiler. A few hundred derived instances are fine; auto derivation across a large codebase is noticeably slower. Prefer derives and semiauto, and keep derived types in modules that change less often.
Kittens derives Cats type classes only; JSON codecs come from the codec library, for example circe, and one type can derive both.
What to do next
- Add kittens next to cats-core and, on Scala 3,
import cats.derived.*in the files that declare types. - Replace hand-written structural Eq, Hash, Order and Monoid instances with derives clauses, keeping hand-written ones only where the meaning is a domain decision.
- Audit every derived Show for fields that must not reach logs, and hand-write those.
- Switch core domain modules to strict derivation so nested instances are always explicit.
- Derive Functor, Foldable and Traverse for your own container types, and hand-write them for deeply recursive ones.
- Add discipline law tests for every derived Monoid, Order and Traverse, reusing the generators in your property tests.