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:

  1. Collect instances that should be derived;
  2. Find a derivation method in the resources/deriving.conf file;
  3. Generate (if it does not exist) a companion object and append necessary instances.

Configuration

  1. 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
}
  1. Describe derivation rules in the resources/deriving.conf file

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
  1. 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.