scalatypeclassscala-macrosscala-3generic-derivation

Type Class Derivation accessing default values


Is there a clean way to access the default values of a case class fields when performing type class derivation in Scala 3 using Mirrors? For example:

case class Foo(s: String = "bar", i: Int, d: Double = Math.PI)

Mirror.Product.MirroredElemLabels will be set to ("s", "i", "d"). Is there anything like: (Some["bar"], None, Some[3.141592653589793])?

If not could this be achieved using Macros? Can I use the Mirrors and Macros simultaneously to derive a type class instance?


Solution

  • You'll have to write a macro working with methods named like <init>$default$1, <init>$default$2, ... in companion object

    import scala.quoted.*
    
    inline def printDefaults[T]: Unit = ${printDefaultsImpl[T]}
    
    def printDefaultsImpl[T](using Quotes, Type[T]): Expr[Unit] = {
      import quotes.reflect.*
    
      (1 to 3).map(i =>
        TypeRepr.of[T].typeSymbol
          .companionClass
          .declaredMethod(s"$$lessinit$$greater$$default$$$i")
          .headOption
          .flatMap(_.tree.asInstanceOf[DefDef].rhs)
      ).foreach(println)
    
     '{()}
    }
    
    printDefaults[Foo]
    //Some(Literal(Constant(bar)))
    //None
    //Some(Select(Ident(Math),PI))
    

    Mirrors and macros can work together:

    import scala.quoted.*
    import scala.deriving.*
    
    trait Default[T] {
      type Out <: Tuple
      def defaults: Out
    }
    
    object Default {
      transparent inline given mkDefault[T](using 
        m: Mirror.ProductOf[T], 
        s: ValueOf[Tuple.Size[m.MirroredElemTypes]]
      ): Default[T] =
        new Default[T] {
          type Out = Tuple.Map[m.MirroredElemTypes, Option]
          def defaults = getDefaults[T](s.value).asInstanceOf[Out]
        }
    
      inline def getDefaults[T](inline s: Int): Tuple = ${getDefaultsImpl[T]('s)}
    
      def getDefaultsImpl[T](s: Expr[Int])(using Quotes, Type[T]): Expr[Tuple] = {
        import quotes.reflect.*
    
        val n = s.asTerm.underlying.asInstanceOf[Literal].constant.value.asInstanceOf[Int]
    
        val terms: List[Option[Term]] =
          (1 to n).toList.map(i =>
            TypeRepr.of[T].typeSymbol
              .companionClass
              .declaredMethod(s"$$lessinit$$greater$$default$$$i")
              .headOption
              .flatMap(_.tree.asInstanceOf[DefDef].rhs)
          )
    
        def exprOfOption[T](oet: Option[Expr[T]])(using Type[T], Quotes): Expr[Option[T]] = oet match {
          case None => Expr(None)
          case Some(et) => '{Some($et)}
        }
    
        val exprs: List[Option[Expr[Any]]] = terms.map(_.map(_.asExprOf[Any]))
        val exprs1: List[Expr[Option[Any]]] = exprs.map(exprOfOption)
        Expr.ofTupleFromSeq(exprs1)
      }
    }
    

    Usage:

    val d = summon[Default[Foo]]
    summon[d.Out =:= (Option[String], Option[Int], Option[Double])] // compiles
    d.defaults // (Some(bar),None,Some(3.141592653589793))