Scala 2: boilerplate-free type class definition
The development of an application requires design decisions. Fairly often a functional application is being designed around ad-hoc polymorphism.
Ad-hoc polymorphism refers to when a value is able to adopt any one of several types because it, or a value it uses, has been given a separate definition for each of those types.
Haskell wiki
Type class is an approach to ad-hoc polymorphism in Scala. Functional libraries widely use this concept: EntityEncoder in http4s, Functor in cats, Encoder in circe, and so on. Since you need an instance of a type class for a specific type it can be either created manually or derived by a library (shapeless, magnolia).
In common, an instance can be defined in a companion object or being derived at a call site. This is the point where the problem starts.

TL;DR
scalaz-deriving is a zero dependency library that provides a boilerplate-free way of defining a type class instance:
@scalaz.annotation.deriving(Eq, Hash, Show, Codec)
final case class User(firstName: String, lastName: String)
The detailed configuration described at the end of the article.
What's wrong with automated derivation
I guess everyone is familiar with wildcard imports that provide a generic derivation of a type class, e.g. import io.circe.generic.auto._ , import ShowMagnolia._ , and so on.
This approach has several disadvantages:
1. A type class is being generated at every call site
It’s not a problem until your application is small or you don’t care about the compilation speed. Example:
import io.circe.generic.auto._
final case class User(firstName: String, lastName: String)
def stringifyNoSpaces(value: User): String =
io.circe.Encoder[User].apply(value).noSpaces // Encoder instance #1
def stringify(value: User): String =
io.circe.Encoder[User].apply(value).toString // Encoder instance #2
def decode(input: String): Either[io.circe.Error, String] =
io.circe.parser.decode[User](input) // Decoder instance #1
2. The output can be different
The problem arises from the previous point. Magnolia can choose a different instance for the underlying type based on the imports. Example:
import ShowMagnolia._
final case class User(firstName: String, lastName: String)
def stringify1(value: User): String = {
import Domain.toUpperCaseStringShow
Show[User].show(value) // Instance #1
}
def stringify2(value: User): String =
Show[User].show(value) // Instance #2
val user = User("John", "Smith")
stringify1(user) // output: User(firstName = JOHN, lastName = SMITH)
stringify2(user) // output: User(firstName = John, lastName = Smith)
The example above is slightly unreal in the real world. But you can get a different behavior due to an accidental wildcard import import Domain._.
Cached instances
An instance can be defined in a companion object and the compiler will reuse it in all necessary places:
import io.circe.Codec
import io.circe.generic.semiauto
final case class User(firstName: String, lastName: String)
object User {
implicit val userCodec: Codec[User] = semiauto.deriveCodec
}
Looks good, isn’t it? The compiler will automatically pick up an instance of the type class from the companion object. The most important thing, that the instance is being created only once, which is good for the compilation speed.
But the further you dive into the development process, the more type classes your model requires: cats.Hash, cats.Eq, cats.Show, and so on.
import io.circe.generic.semiauto
final case class User(firstName: String, lastName: String)
object User {
implicit val userCodec: Codec[User] = semiauto.deriveCodec
implicit val userEq: Eq[User] = Eq.fromUniversalEquals
implicit val userHash: Hash[User] = Hash.fromUniversalHashCode
implicit val userShow: Show[User] = ShowMagnolia.derive
}
Well, this one looks much more polluted. And now imagine that you have dozens of different models. And it quickly turns into a mess.
Looking at the example above we can trace the pattern: the creation of an instance does not require any custom logic. An instance is being created by a utility method and since the implementation depends only on the type, the boilerplate part can be generated by a compiler plugin.
scalaz-deriving plugin
Apart from the name, the compiler plugin does not have any dependency on scalaz. The plugin creates corresponding instances using derivation methods described at deriving.conf.
The plugin logic is pretty straightforward:
- Collect instances that should be derived;
- Find a derivation method in the
resources/deriving.conffile; - Generate (if it does not exist) a companion object and append necessary instances.
Configuration
- Add the compiler plugin and macro library to the
build.sbt
libraryDependencies += "org.scalaz" %% "deriving-macro" % "2.0.0-M7"
addCompilerPlugin("org.scalaz" %% "deriving-plugin" % "2.0.0-M7" cross CrossVersion.full)
If you want to use methods for derivation defined in the same compilation unit (e.g. Magnolia), add to the build.sbt:
Compile / managedClasspath := {
val res = (Compile / resourceDirectory).value
val old = (Compile / managedClasspath).value
Attributed.blank(res) +: old
}
- Describe derivation rules in the
resources/deriving.conffile
The compiler plugin takes the rules from the deriving.conf file, which must be in the resources folder. Rules example:
cats.kernel.Eq = cats.kernel.Eq.fromUniversalEquals
cats.Show = example.ShowMagnolia.derive
cats.kernel.Hash = cats.kernel.Hash.fromUniversalHashCode
io.circe.Codec = io.circe.generic.semiauto.deriveCodec
- Annotate your domain models
@scalaz.annotation.deriving(Eq, Hash, Show, Codec)
final case class User(firstName: String, lastName: String)
The difference is significant, isn’t it? The model looks much cleaner now.
The first thing you can pay attention to is the lack of a companion object. It’s being generated along with the type classes.
Moreover, the plugin supports generic classes as well:
@scalaz.annotation.deriving(Show)
final case class GenericClass[A, B, C](a: A, b: B, c: C)
The generated code:
final case class GenericClass[A, B, C](a: A, b: B, c: C)
object GenericClass {
implicit def _deriving_show[A, B, C](implicit
evidence$$A: Show[A],
evidence$$B: Show[B],
evidence$$C: Show[C]
): Show[GenericClass[A, B, C]] = example.ShowMagnolia.derive
}
As you can see, scalaz-deriving provides a simple way of keeping the models clean.
Instead of a conclusion
Not only scalaz-deriving can help us with a boilerplate. The library derevo provides a similar syntax, but it does not support custom types yet. Also, Scala 3 received a fancy derivation mechanism. But in Scala 2 world, we can rely either on scalaz-deriving or derevo.
Further reading
Magnolia — shapeless-free type class derivation.
Scala 3 Derivation — Scala 3 finally received a fancy derivation mechanism.
Implicits, type classes, and extension methods — explained Scala 2 implicits.