A micro-library to derive a typeclass for Scala 3 Union types.
To use union-derivation
in an existing SBT project with Scala 3.3.1 or a later version.
Configure you project via build.sbt
:
libraryDependencies += "io.github.irevive" %% "union-derivation-core" % "0.2.0"
scalacOptions += "-Yretain-trees" // important for the detection of an abstract method in a trait
Or via scala-cli directives:
//> using scala "3.3.4"
//> using lib "io.github.irevive::union-derivation-core:0.2.0"
//> using options "-Yretain-trees" // important for the detection of an abstract method in a trait
Versions matrix:
Scala | Library | JVM | Scala Native (0.4) | Scala Native (0.5.x) | Scala.js |
---|---|---|---|---|---|
3.1.2 | 0.0.3 | + | - | - | - |
3.2.0+ | 0.0.4+ | + | + | + | - |
3.3.x | 0.1.x | + | + | - | + |
3.3.x | 0.2.x | + | - | + | + |
The library generates a set of if-else statements for the known types of the union.
The simplified version of the generated code:
val instance: Show[Int | String | Long] = UnionDerivation.derive[Show, Int | String | Long]
// expands into
val instance: Show[Int | String | Long] = { (value: Int | String | Long) =>
if (value.isInstanceOf[Int]) summon[Show[Int]].show(value.asInstanceOf[Int])
else if (value.isInstanceOf[String]) summon[Show[String]].show(value.asInstanceOf[String])
else if (value.isInstanceOf[Long]) summon[Show[Long]].show(value.asInstanceOf[Long])
else sys.error("Impossible")
}
import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}
// A typeclass definition
trait Show[A] {
def show(value: A): String
}
// The typeclass instances
given Show[String] = value => s"str: $value"
given Show[Int] = value => s"int: $value"
// Implicit derivation that works only for the union types
inline given derivedUnion[A](using IsUnion[A]): Show[A] =
UnionDerivation.derive[Show, A]
println(summon[Show[String | Int]].show(1))
// int: 1
println(summon[Show[String | Int]].show("1"))
// str: 1
A derivation works for a typeclass with a single extension method too:
import io.github.irevive.union.derivation.UnionDerivation
// A typeclass definition
trait Show[A] {
extension(a: A) def show: String
}
// The typeclass instances
given Show[String] = value => s"str: $value"
given Show[Int] = value => s"int: $value"
// Explicit (manual) derivation for the specific union type
type UnionType = String | Int
given Show[UnionType] = UnionDerivation.derive[Show, UnionType]
println((1: UnionType).show)
// int: 1
println(("1": UnionType).show)
// str: 1
import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}
import scala.compiletime.{erasedValue, summonInline}
import scala.deriving.*
// A typeclass definition
trait Show[A] {
def show(a: A): String
}
object Show extends ShowLowPriority {
def apply[A](using ev: Show[A]): Show[A] = ev
// The typeclass instances
given Show[Int] = v => s"Int($v)"
given Show[Long] = v => s"Long($v)"
given Show[String] = v => s"String($v)"
// The derivation mechanism
// Checkout https://docs.scala-lang.org/scala3/reference/contextual/derivation.html for more details
inline given derived[A](using m: Mirror.Of[A]): Show[A] = {
val elemInstances = summonAll[m.MirroredElemTypes]
inline m match {
case s: Mirror.SumOf[A] => showSum(s, elemInstances)
case _: Mirror.ProductOf[A] => showProduct(elemInstances)
}
}
inline def summonAll[A <: Tuple]: List[Show[?]] =
inline erasedValue[A] match {
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[Show[t]] :: summonAll[ts]
}
private def showA[A](a: A, show: Show[?]): String =
show.asInstanceOf[Show[A]].show(a)
private def showSum[A](s: Mirror.SumOf[A], elems: => List[Show[?]]): Show[A] =
new Show[A] {
def show(a: A): String = showA(a, elems(s.ordinal(a)))
}
private def showProduct[A](elems: => List[Show[?]]): Show[A] =
new Show[A] {
def show(a: A): String = {
val product = a.asInstanceOf[Product]
product.productIterator
.zip(product.productElementNames)
.zip(elems.iterator)
.map { case ((field, name), show) => s"$name = ${showA[Any](field, show)}" }
.mkString(product.productPrefix + "(", ", ", ")")
}
}
}
// Since the 'derivedUnion' is defined in the trait, it's a low-priority implicit
trait ShowLowPriority {
// Implicit derivation that works only for the union types
inline given derivedUnion[A](using IsUnion[A]): Show[A] = UnionDerivation.derive[Show, A]
}
type UnionType = Int | Long | String
final case class User(name: String, age: Long, flags: UnionType)
val unionShow: Show[UnionType] = summon[Show[UnionType]]
// unionShow: Show[UnionType] = repl.MdocSession$MdocApp6$$Lambda/0x0000008003871340@75308f03
val userShow: Show[User] = summon[Show[User]]
// userShow: Show[User] = repl.MdocSession$$anon$18@972b92
println(unionShow.show(1))
// Int(1)
println(unionShow.show(2L))
// Long(2)
println(unionShow.show("3"))
// String(3)
println(userShow.show(User("Pablo", 22, 12L)))
// User(name = String(Pablo), age = Long(22), flags = Long(12))
println(userShow.show(User("Pablo", 33, 1)))
// User(name = String(Pablo), age = Long(33), flags = Int(1))
final case class Author(name: String, age: Long, flags: Long | String) derives Show
println(Show[Author].show(Author("Pablo", 22, 12L)))
// Author(name = String(Pablo), age = Long(22), flags = Long(12))
println(Show[Author].show(Author("Pablo", 33, "string flag")))
// Author(name = String(Pablo), age = Long(33), flags = String(string flag))
A typeclass function without parameters:
trait Typeclass[A] {
def magic: String
// ^
// Polymorphic parameter of type A is missing
}
A typeclass function without polymorphic parameter:
trait Typeclass[A] {
def magic(a: Int): String
// ^
// Polymorphic parameter of type A is missing
}
A polymorphic parameter is mandatory to perform the type matching in runtime.
trait Typeclass[A, B] {
def magic(a: A, b: B): String
}
However, you can overcome this limitation by using polymorphic function types:
trait Typeclass[A] {
def magic(a: A): [B] => B => String
}
trait Typeclass[A] {
def magic(a1: A, b: Int, a2: A): String
}
A polymorphic parameter of type A
appears in two positions. A macro cannot properly detect which type to use.
trait Typeclass[A] {
def magic(a: A)(b: String): A
}
However, you can overcome this limitation by moving currying to the result type definition:
trait Typeclass[A] {
def magic(a: A): String => A
}
The library works out of the box with scala-cli too.
//> using scala "3.3.4"
//> using lib "io.github.irevive::union-derivation-core:0.2.0"
//> using options "-Yretain-trees"
import io.github.irevive.union.derivation.{IsUnion, UnionDerivation}
trait Show[A] {
def show(value: A): String
}
given Show[String] = value => s"str: $value"
given Show[Int] = value => s"int: $value"
inline given derivedUnion[A](using IsUnion[A]): Show[A] = UnionDerivation.derive[Show, A]
println(summon[Show[String | Int]].show(1))
// int: 1
println(summon[Show[String | Int]].show("1"))
// str: 1