Magnolia

fast and unintrusive typeclass derivation

Getting started

SBT

"com.propensive" %% "magnolia" % "0.1.0"

Import

import magnolia._

Links

About

Magnolia is a generic macro for automatic materialization of typeclasses for datatypes composed from case classes (products) and sealed traits (coproducts). It supports recursively-defined datatypes out-of-the-box, and incurs no significant time-penalty during compilation. If derivation fails, error messages are detailed and informative.

Features

Usage

Given an ADT such as,

sealed trait Tree
case class Branch(left: Tree, right: Tree) extends Tree
case class Leaf(value: Int) extends Tree

we can automatically derive implicit typeclass instances such as Show[Tree] on-demand, for example in the code,

Branch(Branch(Leaf(1), Leaf(2)), Leaf(3)).show

provided that a Show instance for Ints exists, and an implicit derivation typeclass — an instance of Derivation or Coderivation — exists in scope for the typeclass being derived. These are simple to write for most typeclasses.

The derivation typeclass for Show might look like this:

implicit val derivation = new Coderivation[Show] {
  type Return = String
  def call[T](show: Show[T], value: T): String = show.show(value)
  def construct[T](body: T => String): Show[T] = body(_)
  
  def join(name: String, xs: ListMap[String]): String =
    xs.values.mkString(s"$name(", ", ", ")")
}

To include Magnolia derivation in your own project, you should include a low-priority implicit which calls the macro, like so:

import language.experimental.macros, magnolia._
implicit def generic[T]: Show[T] = macro Macros.magnolia[T, Show[_]]

Debugging

Deriving typeclasses is not always guaranteed to succeed, though. Many datatypes are complex and deeply-nested, and failure to derive a single type in one of the leaf nodes will cause the entire tree to fail.

Magnolia tries to be informative about why failures occur, by providing a "stack trace" showing the path to the type which could not be derived.

For example, when attempting to derive a Show instance for Entity, given the following hypothetical datatypes,

sealed trait Entity
case class Person(name: String, address: Address) extends Entity
case class Organization(name: String, contacts: Set[Person]) extends Entity
case class Address(lines: List[String], country: Country)
case class Country(name: String, code: String, salesTax: Boolean)

the absence, for example, of a Show[Boolean] typeclass instance would cause derivation to fail, but the reason might not be obvious, so instead, Magnolia will report the following compile error:

could not derive Show instance for type Boolean
    in parameter 'salesTax' of product type Country
    in parameter 'country' of product type Address
    in parameter 'address' of product type Person
    in chained implicit of type Set[Person]
    in parameter 'contacts' of product type Organization
    in coproduct type Entity

How does it work?

A naïve generic, implicit macro would typically fail when trying to derive a typeclass instance for any recursively-defined ADT. It would simply continue calling itself forever, following a cycle of types. Thankfully, the compiler is able to detect these cycles, and abort early with a divergent implicit expansion (DIE) error. Magnolia goes to some lengths to circumvent DIE errors during derivation, and instead of attempting to derive a typeclass instance for a type which has already been computed, it refers back to the typeclass instance created earlier.

In order to achieve this, when an instance of the Magnolia macro initiates implicit search, it detects when that results in a recursive call to itself, and aborts the nested invocation immediately. Then, instead of relying on implicit search for expansion (which is subject to DIE checks), the macro implementation method recurses on itself directly.

This has the advantage that it allows the macro to build a single, complete AST expansion, and have it typechecked just once, at the end. This allows trees to be generated by nested invocations of the macro which refer to typeclass instances defined by surrounding invocations of the macro, without the need to have them typecheck in isolation.

Current Status

Magnolia is currently experimental. It has been shown to work for a variety of contrived test cases, though it has not had the same exposure to real-world datatypes that, for example, Shapeless has had.

The API for defining derivations has been shown to be adequate for the test cases, but is likely to need to evolve as more is learned about different users' requirements.

In terms of production-readiness, the macro will not produce code which fails to typecheck. But it can always refuse to generate a derivation, so users should be cautious of becoming reliant on it until it has received more thorough testing.

However, in all cases it should be possible to write typeclass instances manually, and have these take precedence over Magnolia.

The runtime performance of Magnolia-generated code is also not yet known. It is known that it will generate some ephemeral heap garbage, and this is an area where there is expected to be some improvement in subsequent versions.