Version Matrix

Magnolia

GitHub Workflow

Magnolia

Magnolia is a generic macro for automatic materialization of typeclasses for datatypes composed from product types (e.g. case classes) and coproduct types (e.g. enums). It supports recursively-defined datatypes out-of-the-box, and incurs no significant time-penalty during compilation.

Features

  • derives typeclasses for case classes, case objects and sealed traits
  • offers a lightweight syntax for writing derivations without needing to understand complex parts of Scala
  • builds upon Scala 3's built-in generic derivation
  • works with recursive and mutually-recursive definitions
  • supports parameterized ADTs (GADTs), including those in recursive types
  • supports typeclasses whose generic type parameter is used in either covariant and contravariant positions

Getting Started

Given an ADT such as,

enum Tree[+T] derives Print:
  case Branch(left: Tree[T], right: Tree[T])
  case Leaf(value: T)

and provided an given instance of Print[Int] is in scope, and a Magnolia derivation for the Print typeclass has been provided, we can automatically derive given typeclass instances of Print[Tree[Int]] on-demand, like so,

Tree.Branch(Tree.Branch(Tree.Leaf(1), Tree.Leaf(2)), Tree.Leaf(3)).print

Typeclass authors may provide Magnolia derivations in the typeclass's companion object, but it is easy to create your own.

The definition of a Print typeclass with generic derivation defined with Magnolia might look like this:

import magnolia.*

trait Print[T] {
  extension (x: T) def print: String
}

object Print extends AutoDerivation[Print]:
  def join[T](ctx: CaseClass[Typeclass, T]): Print[T] = value =>
    ctx.params.map { param =>
      param.typeclass.print(param.deref(value))
    }.mkString(s"${ctx.typeInfo.short}(", ",", ")")

  override def split[T](ctx: SealedTrait[Print, T]): Print[T] = value =>
    ctx.choose(value) { sub => sub.typeclass.print(sub.cast(value)) }
  
  given Print[Int] = _.toString

The AutoDerivation trait provides a given autoDerived method which will attempt to construct a corresponding typeclass instance for the type passed to it. Importing Print.autoDerived as defined in the example above will make generic derivation for Print typeclasses available in the scope of the import.

While any object may be used to define a derivation, if you control the typeclass you are deriving for, the companion object of the typeclass is the obvious choice since it generic derivations for that typeclass will be automatically available for consideration during contextual search.

If you don't want to make the automatic derivation available in the given scope, consider using the Derivation trait which provides semi-auto derivation with derived method, but also brings some additional limitations.

Limitations

Magnolia is not currently able to access default values for case class parameters.

For a recursive structures it is required to assign the derived value to an implicit variable e.g.

given instance: SemiPrint[Recursive] = SemiPrint.derived

Availability

For Scala 3:

val magnolia = "com.softwaremill.magnolia" %% "magnolia-core" % "2.0.0-M8"

For Scala 2, see the legacy branch.

Contributing

Contributors to Magnolia are welcome and encouraged. New contributors may like to look for issues marked label: good first issue.

Credits

Magnolia was originally designed and developed by Jon Pretty, and is currently maintained by SoftwareMill.

License

Magnolia is made available under the Apache 2.0 License.