Kittens: automatic type class derivation for Cats
Kittens is a Scala library which provides instances of type classes from the Cats library for arbitrary
algebraic data types (ADTs) using shapeless-based automatic type class derivation. It also provides utility
functions related to Applicative
such as lift
, traverse
and sequence
to HList
, Record
and case classes.
Kittens is part of the Typelevel family of projects. It is an Open Source project under the Apache License v2, hosted on GitHub. Binary artifacts will be published to the Sonatype OSS Repository Hosting service and synced to Maven Central.
It is available for Scala 2.12 and 2.13, Scala.js 1.5 and Scala Native 0.4.
To get started with sbt, simply add the following to your build.sbt
file:
libraryDependencies += "org.typelevel" %% "kittens" % "latestVersion" // indicated in the badge below
Scala 2
Instance derivations are available for the following type classes:
Eq
,PartialOrder
,Order
,Hash
Show
, prettyShow
Empty
,EmptyK
(from Alleycats)Semigroup
,CommutativeSemigroup
,SemigroupK
Monoid
,CommutativeMonoid
,MonoidK
Functor
,Contravariant
,Invariant
Pure
(from Alleycats),Apply
,Applicative
Foldable
,Reducible
Traverse
,NonEmptyTraverse
ConsK
(from Alleycats)
See the Type class support matrix for more details.
Derivation examples
scala> import cats.implicits._, cats._, cats.derived._
scala> case class Cat[Food](food: Food, foods: List[Food])
defined class Cat
scala> val cat = Cat(1, List(2, 3))
cat: Cat[Int] = Cat(1,List(2, 3))
Functor
Derive scala> implicit val fc: Functor[Cat] = semiauto.functor
FC: cats.Functor[Cat] = cats.derived.MkFunctor2$$anon$4@1c60573f
scala> cat.map(_ + 1)
res0: Cat[Int] = Cat(2,List(3, 4))
Show
Derive Note that the derived Show
also prints out field names, so it might be preferable to the default toString
:
scala> case class Address(street: String, city: String, state: String)
scala> case class ContactInfo(phoneNumber: String, address: Address)
scala> case class People(name: String, contactInfo: ContactInfo)
scala> val mike = People("Mike", ContactInfo("202-295-3928", Address("1 Main ST", "Chicago", "IL")))
scala> // existing Show instance for Address
scala> implicit val addressShow: Show[Address] =
a => s"${a.street}, ${a.city}, ${a.state}"
scala> implicit val peopleShow: Show[People] = semiauto.show // auto derive Show for People
scala> mike.show
res0: String = People(name = Mike, contactInfo = ContactInfo(phoneNumber = 202-295-3928, address = 1 Main ST, Chicago, IL))
Note that in this example, the derivation generated instances for all referenced classes but still respected the existing instance in scope. For different ways to derive instances please see the three modes of derivation below.
Sequence examples
Note that to run these examples you need partial unification enabled.
For Scala 2.12 you should add the following to your build.sbt
:
scalacOptions += "-Ypartial-unification"
scala> import cats.implicits._, cats.sequence._
import cats.implicits._
import cats.sequence._
scala> val f1 = (_: String).length
f1: String => Int = <function1>
scala> val f2 = (_: String).reverse
f2: String => String = <function1>
scala> val f3 = (_: String).toFloat
f3: String => Double = <function1>
scala> val f = sequence(f1, f2, f3)
f: String => shapeless.::[Int,shapeless.::[String,shapeless.::[Float,shapeless.HNil]]] = <function1>
scala> f("42.0")
res0: shapeless.::[Int,shapeless.::[String,shapeless.::[Float,shapeless.HNil]]] = 4 :: 0.24 :: 42.0 :: HNil
//or generic over ADTs
scala> case class MyCase(a: Int, b: String, c: Float)
defined class MyCase
scala> val myGen = sequenceGeneric[MyCase]
myGen: cats.sequence.sequenceGen[MyCase] = cats.sequence.SequenceOps$sequenceGen@63ae3243
scala> val f = myGen(a = f1, b = f2, c = f3)
f: String => MyCase = <function1>
scala> f("42.0")
res1: MyCase = MyCase(4,0.24,42.0)
Traverse works similarly except you need a shapeless.Poly
.
Lift examples
scala> import cats._, implicits._, lift._
import cats._
import implicits._
import lift._
scala> def foo(x: Int, y: String, z: Float) = s"$x - $y - $z"
scala> val lifted = Applicative[Option].liftA(foo _)
lifted: (Option[Int], Option[String], Option[Float]) => Option[String] = <function3>
scala> lifted(Some(1), Some("a"), Some(3.2f))
res0: Option[String] = Some(1 - a - 3.2)
Three modes of derivation
Kittens provides three objects for derivation cats.derived.auto
, cats.derived.cached
and cats.derived.semi
The recommended best practice is going to be a semi auto one:
import cats.derived
implicit val showFoo: Show[Foo] = {
import derived.auto.show._
derived.semiauto.show
}
This will respect all existing instances even if the field is a type constructor. For example Show[List[A]]
will use
the native Show
instance for List
and derived instance for A
. And it manually caches the result to the
val showFoo
. Downside user will need to write one for every type they directly need a Show
instance.
There are 3 alternatives:
- full auto:
import derived.auto.show._
The downside is that it will re-derive for every use site, which multiples the compilation time cost.
- full auto cached
import derived.cached.show._
Use this one with caution. It caches the derived instance globally. So it's only applicable if the instance is global
in the application. This could be problematic for libraries, which has no control over the uniqueness of an instance on
use site. It relies on shapeless.Cached
which is buggy.
- manual semi
implicit val showFoo: Show[Foo] = derived.semiauto.show
It has the same downside as the recommenced semi-auto practice but also suffers from the type constructor field issue. I.e. if a field type is a type constructor whose native instance relies on the instance of the parameter type, this approach will by default derive an instance for the type constructor one. To overcome this user have to first derive the instance for type parameter. e.g. given:
case class Foo(bars: List[Bar])
case class Bar(a: String)
Since the bars
field of Foo
is a List
of Bar
which breaks the chains of auto derivation, you will need to
derive Bar
first and then Foo
.
implicit val showBar: Show[Bar] = semiauto.show
implicit val showFoo: Show[Foo] = semiauto.show
This way the native instance for Show[List]
would be used.
Scala 3
We also offer 3 methods of derivation for Scala 3. All of them have the same behaviour wrt to recursively defining instances:
- Instances will always be recursively instantiated if necessary
- Subject to the same type constructor field limitation as the Scala 2 auto and manual semi derivations
derives
syntax (recommended)
Kittens for scala 3 supports Scala 3's derivation syntax.
import cats.derived.*
// No instances declared for Name
case class Name(value: String)
case class Person(name: Name, age: Int) derives Eq, Show
enum CList[+A] derives Functor:
case CNil
case CCons(head: A, tail: CList[A])
semiauto derivation
This looks similar to semiauto
for Scala 2.
import cats.derived.semiauto
// No instances declared for Name
case class Name(value: String)
case class Person(name: Name, age: Int)
object Person:
given Eq[Person] = semiauto.eq
given Show[Person] = semiauto.show
enum CList[+A]:
case CNil
case CCons(head: A, tail: CList[A])
object CList:
given Functor[CList] = semiauto.functor
As with Scala 2, you can combine auto
and semiauto
to avoid the type constructor field limitation:
import cats.derived.*
case class Name(value: String)
case class Person(name: Name, age: Int)
case class People(people: List[Person])
object People:
given Show[People] =
import auto.show.given
// Uses the correct List instance despite deriving an instance for Person automatically
semiauto.show
`
auto derivation
This looks similar to auto
for Scala 2.
import cats.derived.auto.eq.given
import cats.derived.auto.show.given
import cats.derived.auto.functor.given
case class Name(value: String)
case class Person(name: Name, age: Int)
enum CList[+A]:
case CNil
case CCons(head: A, tail: CList[A])
Caveats
Nested type constructors
We are currently unable to
derive instances for nested type constructors, such as Functor[[x] =>> List[Set[x]]]
.
Stack safety
Our derived instances are not stack-safe. This is a departure from the behaviour for Scala 2 because we didn't want to incur the performance penalty of trampolining all instances in cats.Eval
. If your data-type is recursive or extremely large then you may want to write instances by hand instead.
Missing features
Kittens for Scala 3 is built on top of Shapeless
3 which has a completely different
API than Shapeless 2 so we don't
support features like Sequence
and Lift
.
ConsK
derivation is also not supported although we expect this to be
added in a future release.
Type class support matrix
Legend:
โ
- all must satisfy a constraintโ
- at lest one must satisfy a constraintโ!
- exactly one must satisfy a constraintโง
- both constraints must be satisfiedโจ
- either constraint must be satisfied
For monomorphic types
Type Class | Case Classes | Sealed Traits |
---|---|---|
CommutativeMonoid | โ fields: CommutativeMonoid | |
CommutativeSemigroup | โ fields: CommutativeSemigroup | |
Empty | โ fields: Empty | โ variant: Empty |
Eq | โ fields: Eq | โ variants: Eq |
Hash | โ fields: Hash | โ variants: Hash |
Monoid | โ fields: Monoid | |
Order | โ fields: Order | โ! variant: Order |
PartialOrder | โ fields: PartialOrder | โ variants: PartialOrder |
Semigroup | โ fields: Semigroup | |
Show | โ fields: Show | โ variants: Show |
ShowPretty | โ fields: ShowPretty | โ variants: ShowPretty |
For polymorphic types
Type Class | Case Classes | Sealed Traits | Constant Types ฮป[x => T] |
Nested Types ฮป[x => F[G[x]]] |
---|---|---|---|---|
Applicative | โ fields: Applicative | for T: Monoid | for F: Applicative and G: Applicative | |
Apply | โ fields: Apply | for T: Semigroup | for F: Apply and G: Apply | |
Contravariant | โ fields: Contravariant | โ variants: Contravariant | for any T | for F: Functor and G: Contravariant |
EmptyK | โ fields: EmptyK | for T: Empty | for F: EmptyK and any G โจ for F: Pure and G: EmptyK | |
Foldable | โ fields: Foldable | โ variants: Foldable | for any T | for F: Foldable and G: Foldable |
Functor | โ fields: Functor | โ variants: Functor | for any T | for F: Functor and G: Functor โจ for F: Contravariant and G: Contravariant |
Invariant | โ fields: Invariant | โ variants: Invariant | for any T | for F: Invariant and G: Invariant |
MonoidK | โ fields: MonoidK | for T: Monoid | for F: MonoidK and any G โจ for F: Applicative and G: MonoidK | |
NonEmptyTraverse | โ field: NonEmptyTraverse โง โ fields: Traverse | โ variants: NonEmptyTraverse | for F: NonEmptyTraverse and G: NonEmptyTraverse | |
Pure | โ fields: Pure | for T: Empty | for F: Pure and G: Pure | |
Reducible | โ field: Reducible โง โ fields: Foldable | โ variants: Reducible | for F: Reducible and G: Reducible | |
SemigroupK | โ fields: SemigroupK | for T: Semigroup | for F: SemigroupK and any G โจ for F: Apply and G: SemigroupK | |
Traverse | โ fields: Traverse | โ variants: Traverse | for any T | for F: Traverse and G: Traverse |
Participation
The Kittens project supports the Scala code of conduct and wants all of its channels (mailing list, Gitter, GitHub, etc.) to be welcoming environments for everyone.
Building kittens
Kittens is built with SBT 1.x, and its master branch is built with Scala 2.13 by default.
Contributors
- Cody Allen [email protected] @fourierstrick
- Georgi Krastev [email protected] @Joro_Kr
- Fabio Labella [email protected] @SystemFw
- Miles Sabin [email protected] @milessabin
- Qi Wang Qi77Qi
- Kailuo Wang [email protected] @kailuowang
- Tim Spence [email protected] timwspence
- Your name here :-)