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.