Beginner's guide to derivations in Scala
When learning about Scala, there are a few things that might be surprising. One of such mechanisms is code derivation. With this post I want to demystify the term for those of you who are learning Scala and functional programming in general.
TL;DR This post is intended for Scala beginners and does not go into the details of building your own derivation. Instead, it shows the client side of things, aiming to show the impact on the code simplification.
Different types of classes
If you have programmed in any modern programming language, you definitely know what class
is. The typical class can have properties (context bound variables) and methods (context bound functions).
The textbook examples usually combine both when introducing the concept like this:
class Person(val name: String, val surname: String, val age: Int) {
def sayHello: Unit = println(s"Hello! My name is $name")
}
While this might be the typical approach you might have seen, used a lot in object oriented programming. It’s usually followed by inheritance for building more complex programs.
In functional programming approach, we’d take a different path. Instead of binding the logic to specific data structure, we’d model the domain separately from the services that manipulate it.
Record types aka case classes aka data classes
This is why Scala introduces the concept of a case class
. This is a kind of class that aims to describe a model. It can still have it’s own methods if you need them, but those are for convenience methods rather than business logic. If you are looking for an analogy, think of Python’s data classes or data classes in Kotlin.
If we were to model a person using case class
it would look like this:
case class Person(name: String, surname: String, age: Int)
Case classes in Scala are very handy, they are assumed to be immutable and come with few very useful convenience methods like copy
or equals
. In this post I don’t want to go into too much detail, so if you are interested check out the details in Domain Modeling Tools chapter of Scala Book.
Service classes
The other kind of classes we typically go for, be it in object oriented or functional approach. When creating a data class/record type, you start with it’s properties/fields as this is what you intend to model. Building the service however starts with the idea of manipulating the existing data types. This is where “programming to an interface” comes really handy. Let’s have a look at the example.
trait BasketService {
def calculatePrice(basketContent: List[Item]): Price
}
In this example Item
and Price
could be data classes that represent the model, whereas BasketService
provides the means to manipulate them.
The above example shows a typical business service, but we would use similar approach to utilities, like parsing, database connection or message handling.
Service vs Type Class
The example we’ve shown works with specific data types, since they only make sense in this scenario. In opposite to that, you could define a service-like thing that works with generic types. Let’s see an example
trait Show[A] {
def show(a: A): String
}
We call those type classes as they build up a class of types, because Show[A]
can generate Show[String], Show[Int], Show[Item], Show[List[Price]]
and so on.
Boilerplate
Boilerplate means the repetitive, burdensome code you don’t want to write but you have to. When modeling the data with case classes
we arguably felt no overhead. When implementing the business logic, you usually just provide one reasonable implementation for production code. This falls apart when you want to work with type classes. If we were to implement the Show
for Person
this is what it would look like
object Show {
def apply[A](implicit ev: Show[A]): Show[A] = ev
given Show[String] = new Show[String] {
def show(a: A): String
}
given Show[Int] = new Show[Int] {
def show(a: Int): String = a.toString()
}
}
val ShowPerson = new Show[Person] {
def show(a: Person): String =
s"Person(name = ${Show[String].show(a.name)}, surname = ${Show[String].show(a.surname)}, age = ${Show[String].show(a.age)})"
}
This doesn’t look very clean, especially on the “client side” where you get to use this API. One way to make it simpler would be to use extension methods like this:
object Show {
def apply[A](implicit ev: Show[A]): Show[A] = ev
given Show[String] = new Show[String] {
def show(a: A): String
}
given Show[Int] = new Show[Int] {
def show(a: Int): String = a.toString()
}
extension [A: Show](a: A) {
def show: String = Show[A].show(a)
}
}
val ShowPerson = new Show[Person] {
def show(a: Person): String =
s"Person(name = ${a.name.show}, surname = ${a.surname.show}, age = ${a.age.show})"
}
Reads much better now, but there’s still one major problem. We still need to implement all those things manually. On one hand this sounds just like a job for the programmer - write the code. On the other hand, it feels like Show
for any case class can be automatically generated by following the pattern s"ClassName(param1 = value, param2 = value, ...)
.
Let the compiler write your code aka type class derivation
Fortunately, the Show
type class is already implemented in Cats. It comes with the implementation for basic types built in, quoting the source file:
implicit def catsShowForUnit: Show[Unit] = cats.instances.unit.catsStdShowForUnit
implicit def catsShowForBoolean: Show[Boolean] = cats.instances.boolean.catsStdShowForBoolean
implicit def catsShowForByte: Show[Byte] = cats.instances.byte.catsStdShowForByte
implicit def catsShowForShort: Show[Short] = cats.instances.short.catsStdShowForShort
implicit def catsShowForInt: Show[Int] = cats.instances.int.catsStdShowForInt
That’s part of the job done, but we still want to avoid writing the implementation for our domain types.
Meet type class derivation
The operation of making the compiler come up with the implementation for our logic is called type class derivation. Since this is the beginner introduction, meta-programming techniques behind the derivations are a topic for another post. Let’s focus on making the compiler do our job with the available libraries.
Fortunately for our example, Typelevel provides the kittens project that provides the derivations. All we have to do is use it by importing cats.derived.*
.
//> using dep "org.typelevel::kittens:3.0.0"
//> using dep "org.typelevel::cats-core:2.9.0"
import cats.Show
import cats.implicits.given
import cats.derived.*
case class Person(name: String, surname: String, age: Int)
given Show[Person] = semiauto.show
val testPerson = Person("Test", "Person", 100)
@main
def printPerson() =
println(testPerson.show)
The script produces following output:
Person(name = Test, surname = Person, age = 100)
You can run it for yourself using scala-cli
scala-cli run https://gist.github.com/majk-p/0aafb60db8c99cff4686d5d7e6304c4c
Other examples
Obviously the Show
type class is the simplest possible example that doesn’t achieve much on it’s own. This was just a showcase of what we can get with type class derviations. There are plenty more advanced examples, you might have used them without noticing. Here’s a non-exhaustive table of some popular Scala libraries that provide type class derivation.
Library | Link | Purpose | Description |
---|---|---|---|
Circe | https://circe.github.io/circe/codecs/semiauto-derivation.html | JSON encoding/decoding | Automatic JSON codec derivation for user defined case class |
Doobie | https://tpolecat.github.io/doobie/docs/04-Selecting.html#multi-column-queries | Functional JDBC layer - working with SQL databases | Decodes data from SQL query result to user provided case class |
PureConfig | https://github.com/pureconfig/pureconfig#quick-start | Handling app configuration | Decodes the HOCON/.properties/JSON configuration files to user provided case class |
Phobos | https://github.com/Tinkoff/phobos | XML encoding/decoding | Automatic XML encoder/decoder derivation for user defined case class |
Summary
Type class derivation is a very powerful mechanism provided by Scala compiler. It allows us to eliminate a lot of boilerplate, while remaining very simple on the user side. Keep in mind that, like in case of Cats, sometimes the derivations for type classes you use are provided by different libraries.