project__oxygen_meta - Kalin-Rudnicki/Oxygen GitHub Wiki

oxygen-meta

Summary

Typeclass derivation for the enlightened. The opinion of this library is that Mirrors are dead.
With the right utilities, deriving typeclasses using macros is easier, safer, and generates significantly more efficient code.

Check out the macro video series for some good examples to get started!

Use This Library

Release Artifacts

libraryDependencies += "io.github.kalin-rudnicki" %% "oxygen-meta" % "<latest-version-above>"

Setting Up a Deriver

Basic setup:

import oxygen.meta.K0.*
import scala.quoted.*

trait Example[A] {
  def fun1: String
  def fun2(a: A, b: A): A
}
object Example extends Derivable[Example] {

  override protected def productDeriver[A](using
      Quotes,
      Type[Example],
      Type[A],
      ProductGeneric[A],
      Derivable[Example],
  ): Derivable.ProductDeriver[Example, A] =
    Derivable.ProductDeriver.withInstances[Example, A] { instances =>
      new Derivable.ProductDeriver[Example, A] {

        private def fun1Impl: Expr[String] = '{ ??? }
        private def fun2Impl(a: Expr[A], b: Expr[A]): Expr[A] = '{ ??? }

        override def derive: Expr[Example[A]] =
          '{
            new Example[A] {
              override def fun1: String = $fun1Impl
              override def fun2(a: A, b: A): A = ${ fun2Impl('a, 'b) }
            }
          }

      }
    }

  override protected def sumDeriver[A](using
      Quotes,
      Type[Example],
      Type[A],
      SumGeneric[A],
      Derivable[Example],
  ): Derivable.SumDeriver[Example, A] =
    Derivable.SumDeriver.withInstances[Example, A] { instances =>
      new Derivable.SumDeriver[Example, A] {

        private def fun1Impl: Expr[String] = '{ ??? }
        private def fun2Impl(a: Expr[A], b: Expr[A]): Expr[A] = '{ ??? }

        override def derive: Expr[Example[A]] =
          '{
            new Example[A] {
              override def fun1: String = $fun1Impl
              override def fun2(a: A, b: A): A = ${ fun2Impl('a, 'b) }
            }
          }

      }
    }

  /**
    * Unfortunately, scala macros do not allow this to be implemented in [[Derivable]].
    * Therefore, every companion object that extends [[Derivable]] must implement this function with the following body:
    *      ${ derivedImpl[A] }
    */
  override inline def derived[A]: Example[A] = ${ derivedImpl[A] }

}

Split derivers out into separate classes:

import oxygen.meta.K0.*
import scala.quoted.*

trait Example[A] {
  def fun1: String
  def fun2(a: A, b: A): A
}
object Example extends Derivable[Example] {

  override protected def productDeriver[A](using Quotes, Type[Example], Type[A], ProductGeneric[A], Derivable[Example]): Derivable.ProductDeriver[Example, A] =
    Derivable.ProductDeriver.withInstances[Example, A] { ExampleProductDeriver[A](_) }

  override protected def sumDeriver[A](using Quotes, Type[Example], Type[A], SumGeneric[A], Derivable[Example]): Derivable.SumDeriver[Example, A] =
    Derivable.SumDeriver.withInstances[Example, A] { ExampleSumDeriver[A](_) }

  /**
    * Unfortunately, scala macros do not allow this to be implemented in [[Derivable]].
    * Therefore, every companion object that extends [[Derivable]] must implement this function with the following body:
    *      ${ derivedImpl[A] }
    */
  override inline def derived[A]: Example[A] = ${ derivedImpl[A] }

}

final class ExampleProductDeriver[A](instances: Expressions[Example, A])(using Quotes, Type[Example], Type[A], ProductGeneric[A], Derivable[Example]) extends Derivable.ProductDeriver[Example, A] {

  private def fun1Impl: Expr[String] = '{ ??? }
  private def fun2Impl(a: Expr[A], b: Expr[A]): Expr[A] = '{ ??? }

  override def derive: Expr[Example[A]] =
    '{
      new Example[A] {
        override def fun1: String = $fun1Impl
        override def fun2(a: A, b: A): A = ${ fun2Impl('a, 'b) }
      }
    }

}

final class ExampleSumDeriver[A](instances: Expressions[Example, A])(using Quotes, Type[Example], Type[A], SumGeneric[A], Derivable[Example]) extends Derivable.SumDeriver[Example, A] {

  private def fun1Impl: Expr[String] = '{ ??? }
  private def fun2Impl(a: Expr[A], b: Expr[A]): Expr[A] = '{ ??? }

  override def derive: Expr[Example[A]] =
    '{
      new Example[A] {
        override def fun1: String = $fun1Impl
        override def fun2(a: A, b: A): A = ${ fun2Impl('a, 'b) }
      }
    }

}

Use a _.Split deriver to easily derive differently depending on the specific type of generic you have:

new Derivable.ProductDeriver.Split[Example, A] {

  override def deriveCaseClass(generic: ProductGeneric.CaseClassGeneric[A]): Expr[Example[A]] = ???

  override def deriveAnyVal[B: Type](generic: ProductGeneric.AnyValGeneric[A, B]): Expr[Example[A]] = ???

  override def deriveCaseObject(generic: ProductGeneric.CaseObjectGeneric[A]): Expr[Example[A]] = ???

}

new Derivable.SumDeriver.Split[Example, A] {

  override def deriveFlat(generic: SumGeneric.FlatGeneric[A]): Expr[Example[A]] = ???

  override def deriveEnum(generic: SumGeneric.EnumGeneric[A]): Expr[Example[A]] = ???

  override def deriveNested(generic: SumGeneric.NestedGeneric[A]): Expr[Example[A]] = ???

}

Controlling Derivation

Control how sealed trait hierarchies derive a generic:

object Example extends Derivable[Example] {

  override protected val deriveConfig: Derivable.Config = Derivable.Config(defaultUnrollStrategy = SumGeneric.UnrollStrategy.Nested)

}

How this behaves:

// types:
sealed trait Sum1
sealed trait Sum2 extends Sum1
final case class A() extends Sum2
final case class B() extends Sum2
sealed trait Sum3 extends Sum1
final case class C() extends Sum3
final case class D() extends Sum3

// UnrollStrategy.Unroll (default behavior)
SumGeneric[Sum1](
  ProductGeneric[A],
  ProductGeneric[B],
  ProductGeneric[C],
  ProductGeneric[D],
)

// UnrollStrategy.Nested
SumGeneric[Sum1](
  SumGeneric[Sum2](
    ProductGeneric[A],
    ProductGeneric[B],
  ),
  SumGeneric[Sum3](
    ProductGeneric[C],
    ProductGeneric[D],
  ),
)

When using mirrors, the forced behavior is that of UnrollStrategy.Unroll, but with Oxygen, you have a choice!

An example where one might find this useful, is if you wanted showing an A to appear like Sum1.Sum2.A instead of Sum1.A.

Structure

trait Entity[A]
object Entity {
  trait Child[B, A] extends Entity[B]
}

trait Generic extends Entity[A] {
  type Bound <: Any
  type Child[B <: Bound] <: Entity.Child[B, A]
  final type AnyChild = Child[Bound]
  def children: Contiguous[AnyChild] // fields/cases of this generic
}

trait ProductOrSumGeneric extends Generic[A]

trait ProductGeneric extends ProductOrSumGeneric[A] {
  type Bound = Any // fields can be of any type
  type Child[B] = Field[B]
  final class Field[B] extends Entity.Child[B, A]
}

trait SumGeneric extends ProductOrSumGeneric[A] {
  type Bound = A // cases are a sub-type of A
  type Child[B <: A] = Case[B]
  final class Case[B <: A] extends Entity.Child[B, A]
}

trait IdentityGeneric extends Generic[A]

Child Functions

Most operations using a generic will end up looking something like this:

// ProductGeneric
val fieldNames: Growable[String] =
  generic.mapChildren.map[String] {
    [b] => // no bound
      (_, _) ?=> // provides you an implicit Type[b] and Quotes instance
        (field: generic.Field[b]) => // child
          field.name // do stuff here
  }

// SumGeneric
val fieldNames: Growable[String] =
  generic.mapChildren.map[String] {
    [b <: A] => // bound comes into play here
      (_, _) ?=> // provides you an implicit Type[b] and Quotes instance
        (kase: generic.Case[b]) => // child
          kase.name // do stuff here
  }

This allows you to work with the children of a Generic in a type-safe manner.

If you are ever working with one of these functions, and need to do an asInstanceOf to get the types to work out, you have almost certainly done something wrong.

Instantiating a Product Type

ProductGeneric provides:

def fieldsToInstance[S[_]: SeqOps](exprs: S[Expr[?]])(using Quotes): Expr[A]

This is a very "raw" way to instantiate an A based on a seq of Expr[?]. In most general cases, it's unlikely that this function is your best choice.
You most likely want:

  • generic.instantiate.id { ... } : if you can create an Expr[b] for each field, then you can instantiate an Expr[A]
  • generic.instantiate.option { ... } : if you can create an Expr[b] for each field, then you can instantiate an Expr[A]
  • generic.instantiate.either[L] { ... } : if you can create an Expr[Either[L, b]] for each field, then you can instantiate an Expr[Either[L, A]]
  • generic.instantiate.monad[F] { ... } : if you can create an Expr[F[b]] for each field, then you can instantiate an Expr[F[A]], requires an ExprMonad[F].

.monad and its short-hands (option, either), will generate code that looks like:

for {
  first <- ??? : Either[L, String]
  last <- ??? : Either[L, String]
  age <- ??? : Either[L, Int]
} yield new Person(first, last, age)

// ??? is a placeholder for the expression generated in the ChildFunction

CaseObjectGeneric has a special helper generic.instantiate.instance, as there are no fields to operate on.

AnyValGeneric has a special helper generic.anyVal.wrap(_) and generic.anyVal.unwrap(_), since AnyVals can only have a single field.
Because of this, it also has a more specific generic.field instead of just generic.fields.

Instantiate Examples

private def zeroImpl: Expr[A] =
  generic.instantiate.id {
    [b] =>
      (_, _) ?=>
        (field: generic.Field[b]) =>
          val inst: Expr[Monoid[b]] = field.getExpr(instances)
          '{ $inst.zero }
  }

private def addImpl(expr1: Expr[A], expr2: Expr[A]): Expr[A] =
  generic.instantiate.id {
    [b] =>
      (_, _) ?=>
        (field: generic.Field[b]) =>
          val inst: Expr[Monoid[b]] = field.getExpr(instances)
          val b1: Expr[b] = field.fromParent(expr1)
          val b2: Expr[b] = field.fromParent(expr2)
          '{ $inst.add($b1, $b2) }
  }

Matching on a Sum Type

SumGeneric has generic.matcher.make, as well as helpers for generic.matcher.instance, generic.matcher.instance2, generic.matcher.instance3, and generic.matcher.value.
If one of the more specialized helpers doesn't fit your needs, make will get the job done. The helpers are just one line aliases for make.
The only additional bit of complexity of using make directly is specifying your In[_] type, and potentially needing to manually add a case _ => ... at the end of your matcher.

Match Examples

private def showImpl(a: Expr[A]): Expr[String] =
  instance[String](a) {
    [b <: A] =>
      (_, _) ?=>
        (kase: generic.Case[b]) =>
          val inst: Expr[Show[b]] = kase.getExpr(instances)
          kase.caseExtractor("value").withRHS { bValue => '{ $inst.show($bValue) } }
  }

private def isEqImpl(a: Expr[A], b: Expr[A]): Expr[Boolean] =
  instance2[Boolean](a, b) {
    [b <: A] =>
    (_, _) ?=>
      (kase: generic.Case[b]) =>
        val inst: Expr[Eq[b]] = kase.getExpr(instances)
        (kase.caseExtractor("value1") ++ kase.caseExtractor("value2")).withRHS { case (bValue1, bValue2) => '{ $inst.isEq($bValue1, $bValue2) } }
  }
⚠️ **GitHub.com Fallback** ⚠️