Search code examples
scalametaprogrammingscala-macrosscala-3

Scala3: Crafting Types Through Metaprogramming?


I am coding using scala3, leveraging programmatic structural types. The structural types happen to mimic existing case classes: their definition is pure boiler plate, hence the temptation to craft them through meta-programming.

I understand how to craft a function implementation, typically via typeclass derivation. But here we are trying to craft a (structural) type.

This was possible in scala2, via class macro annotation, but those are gone in scala3. Is there a way ? If so how ?

Code below is the result I would like to obtain :


// Library part
trait View extends scala.Selectable :
  def selectDynamic(key:String) =
    println(s"$key is being looked up")
    ???


// DSL Definition part
case class SomeDefWithInt   ( i : Int    )
case class SomeDefWithString( s : String )

// Boiler-plate code 
type ViewOf[M] = M match
  case SomeDefWithInt    => View { def i : Int    }
  case SomeDefWithString => View { def s : String }

// Mockup usage
class V extends View
val v = V()

v.asInstanceOf[ViewOf[SomeDefWithInt   ]].i
v.asInstanceOf[ViewOf[SomeDefWithString]].s

is it possible to create ViewOf[M] of an arbitrary case class M ?

Thank you !


Solution

  • Just in case, here is what I meant by hiding ViewOf inside a type class (type classes is an alternative to match types). Sadly, in Scala 3 this is wordy.

    (version 1)

    import scala.annotation.experimental
    import scala.quoted.{Expr, Quotes, Type, quotes}
    
    // Library part
    trait View extends Selectable {
      def applyDynamic(key: String)(args: Any*): Any = {
        println(s"$key is being looked up with $args")
        if (key == "i") 1
        else if (key == "s") "a"
        else ???
      }
    
      def selectDynamic(key: String): Any = {
        println(s"$key is being looked up")
        if (key == "i1") 2
        else if (key == "s1") "b"
        else ???
      }
    }
    
    // type class
    trait ViewOf[M <: Product] {
      type Out <: View
    }
    
    object ViewOf {
      transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}
    
      @experimental // because .newClass is @experimental
      def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
        import quotes.reflect.*
    
        extension (symb: Symbol) {
          def setFlags(flags: Flags): Symbol = {
            given dotty.tools.dotc.core.Contexts.Context =
              quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
            symb.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
              .denot.setFlag(flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet])
            symb
          }
        }
    
        def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
          given dotty.tools.dotc.core.Contexts.Context =
            quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
          import dotty.tools.dotc.core.Decorators.toTypeName
          dotty.tools.dotc.core.Symbols.newSymbol(
            cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
            name.toTypeName,
            flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
            tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
            privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
          ).asInstanceOf[Symbol]
        }
    
        val M = TypeRepr.of[M]
        val fields = M.typeSymbol.caseFields
    
        val viewImplDecls = (cls: Symbol) =>
          fields.flatMap(fieldSymb =>
            Seq(
              Symbol.newMethod(cls, fieldSymb.name, MethodType(Nil)(_ => Nil, _ => M.memberType(fieldSymb)), // vararg? MatchError: Inlined
                Flags.Deferred, privateWithin = Symbol.noSymbol),
              Symbol.newVal(cls, fieldSymb.name + "1", M.memberType(fieldSymb),
                Flags.Deferred, privateWithin = Symbol.noSymbol)
            )
          )
    
        val viewImplParents = List(TypeTree.of[AnyRef], TypeTree.of[View])
    
        val viewImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewImpl", viewImplParents.map(_.tpe), viewImplDecls, selfType = None)
          .setFlags(Flags.Trait)
        val methodDefs = fields.flatMap(fieldSymb => {
          val methodSymb = viewImplCls.declaredMethod(fieldSymb.name).head
          val valSymb = viewImplCls.fieldMember(fieldSymb.name + "1")
          Seq(
            DefDef(methodSymb, _ => None),
            ValDef(valSymb, None)
          )
        })
        val viewImplClsDef = ClassDef(viewImplCls, viewImplParents, body = methodDefs)
    
        val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
          TypeBounds(viewImplCls.typeRef, viewImplCls.typeRef), Flags.Override))
    
        val viewOfTypeTree = TypeTree.of[ViewOf[M]]
        val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)
    
        val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
        val outSymb = viewOfImplCls.declaredType("Out").head
    
        val outTypeDef = TypeDef(outSymb)
        val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
        val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)
    
        val res = Block(List(viewImplClsDef, viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
        println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
        res
      }
    }
    
    extension (v: View) {
      def refine[M <: Product](using viewOf: ViewOf[M]): viewOf.Out = v.asInstanceOf[viewOf.Out]
    }
    
    // DSL Definition part
    case class SomeDefWithInt   ( i : Int    )
    case class SomeDefWithString( s : String )
    
    // Mockup usage
    class V extends View
    val v = V()
    
    println(v.refine[SomeDefWithInt].i())
    // i is being looked up with ArraySeq()
    // 1
    println(v.refine[SomeDefWithString].s())
    // s is being looked up with ArraySeq()
    // a
    println(v.refine[SomeDefWithInt].i1)
    // i1 is being looked up
    // 2
    println(v.refine[SomeDefWithString].s1)
    // s1 is being looked up
    // b
    
    //scalac: {
    //  trait ViewImpl extends java.lang.Object with Macros.View {
    //    def i(): scala.Int
    //    val i1: scala.Int
    //  }
    //  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithInt] {
    //    type Out // actually, type Out = ViewImpl
    //  }
    //  new ViewOfImpl()
    //}=Block(List(ClassDef("ViewImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(DefDef("i", List(TermParamClause(Nil)), Inferred(), None), ValDef("i1", Inferred(), None))), ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))
    
    //scalac: {
    //  trait ViewImpl extends java.lang.Object with Macros.View {
    //    def s(): scala.Predef.String
    //    val s1: scala.Predef.String
    //  }
    //  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithString] {
    //    type Out // actually, type Out = ViewImpl
    //  }
    //  new ViewOfImpl()
    //}=Block(List(ClassDef("ViewImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(DefDef("s", List(TermParamClause(Nil)), Inferred(), None), ValDef("s1", Inferred(), None))), ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))
    

    ViewOf[M] is meant is to be used by a DSL user, so no way to hide it within a derived type class.

    Not sure I understood.


    Method Override with Scala 3 Macros

    `tq` equivalent in Scala 3 macros

    How to generate a class in Dotty with macro?

    How to splice multiple expressions in quote syntax of scala 3 macros?

    How to access parameter list of case class in a dotty macro

    https://github.com/lampepfl/dotty/discussions/14056


    Another implementation of the type class (with a refinement type instead of trait type)

    (version 2)

    trait ViewOf[M <: Product] {
      type Out <: View
    }
    
    object ViewOf {
      transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}
    
      @experimental // because .newClass is @experimental
      def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
        import quotes.reflect.*
    
        def makeRefinement(parent: TypeRepr, names: List[String], infos: List[TypeRepr]): TypeRepr =
          names.zip(infos).foldLeft(parent){ case (acc, (name, tpe)) => Refinement(acc, name, tpe) }
    
        def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
          given dotty.tools.dotc.core.Contexts.Context =
            quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
          import dotty.tools.dotc.core.Decorators.toTypeName
          dotty.tools.dotc.core.Symbols.newSymbol(
            cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
            name.toTypeName,
            flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
            tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
            privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
          ).asInstanceOf[Symbol]
        }
    
        val M = TypeRepr.of[M]
        val fields = M.typeSymbol.caseFields
    
        val fieldNames = fields.flatMap(fieldSymb => Seq(fieldSymb.name, fieldSymb.name + "1"))
        val fieldMethodTypes = fields.flatMap(fieldSymb => Seq(
          MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => M.memberType(fieldSymb)),
          ByNameType(M.memberType(fieldSymb)))
        )
        val refinement = makeRefinement(TypeRepr.of[View], fieldNames, fieldMethodTypes)
    
        val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
          TypeBounds(refinement, refinement), Flags.Override))
    
        val viewOfTypeTree = TypeTree.of[ViewOf[M]]
        val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)
    
        val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
        val outSymb = viewOfImplCls.declaredType("Out").head
    
        val outTypeDef = TypeDef(outSymb)
        val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
        val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)
    
        val res = Block(List(viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
        println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
        res
      }
    }
    
    println(v.refine[SomeDefWithInt].i(10, "x", true))
    //i is being looked up with ArraySeq((10,x,true))
    //1
    println(v.refine[SomeDefWithString].s(20, "y", 30L))
    //s is being looked up with ArraySeq((20,y,30))
    //a
    println(v.refine[SomeDefWithInt].i1)
    //i1 is being looked up
    //2
    println(v.refine[SomeDefWithString].s1)
    //s1 is being looked up
    //b
    
    //scalac: {
    //  class ViewOfImpl extends java.lang.Object with Macros.ViewOf[App.SomeDefWithInt] {
    //    type Out // actually, type Out = View {def i(args: Any*): Int; def i1: Int}
    //  }
    //  new ViewOfImpl()
    //}=Block(List(ClassDef("ViewOfImpl", DefDef("<init>", Nil, Inferred(), None), List(Inferred(), Inferred()), None, List(TypeDef("Out", TypeBoundsTree(Inferred(), Inferred()))))), Apply(Select(New(Inferred()), "<init>"), Nil))
    

    Also we can use Mirror instead of reflection

    (version 3)

    trait ViewOf[M <: Product] {
      type Out <: View
    }
    
    object ViewOf {
      transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}
    
      def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
        import quotes.reflect.*
    
        def makeRefinement(parent: TypeRepr, namesAndTypes: List[(String, TypeRepr)]): TypeRepr =
          namesAndTypes.foldLeft(parent) { case (acc, (name, tpe)) => Refinement(acc, name, tpe) }
    
        def mkNamesAndTypes[mels: Type, mets: Type]: List[(String, TypeRepr)] =
          (Type.of[mels], Type.of[mets]) match {
            case ('[EmptyTuple], '[EmptyTuple]) => Nil
            case ('[mel *: melTail], '[met *: metTail] ) => {
              val name = Type.valueOfConstant[mel].get.toString
              val name1 = name + "1"
                //scala.MatchError: Inlined(Ident(Macros$),List(),Apply(Select(New(Select(Select(Select(Ident(scala),annotation),internal),Repeated)),<init>),List())) (of class dotty.tools.dotc.ast.Trees$Inlined)
              //val methodType = MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => TypeRepr.of[met])
              val methodType = MethodType(Nil)(_ => Nil, _ => TypeRepr.of[met])
              val methodType1 = ByNameType(TypeRepr.of[met])
              (name, methodType) :: (name1, methodType1) :: mkNamesAndTypes[melTail, metTail]
            }
          }
    
        val namesAndTypes = Expr.summon[Mirror.ProductOf[M]].get match {
          case '{ $m: Mirror.ProductOf[M] { type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
            mkNamesAndTypes[mels, mets]
        }
    
        val res = makeRefinement(TypeRepr.of[View], namesAndTypes).asType match {
          case '[tpe] =>
            '{
              new ViewOf[M] {
                type Out = tpe
              }
            }
        }
    
        println(res.show)
        res
      }
    }
    

    Unfortunately, this doesn't work because of an extra type ascription (Expr looses type refinement)

    //scalac: {
    //  final class $anon() extends Macros.ViewOf[App.SomeDefWithInt] {
    //    type Out = Macros.View {
    //      def i(): scala.Int
    //      def i1: scala.Int
    //    }
    //  }
    //
    //  (new $anon(): Macros.ViewOf[App.SomeDefWithInt])  // <--- HERE!!!
    //}
    

    https://github.com/lampepfl/dotty/issues/15566 (for structural refinements i.e. defs, their loss seems to be expected behavior, but type refinement loss can be a bug)

    So, at least once we have to use low-level newClass to avoid type ascription

    (version 4)

    trait ViewOf[M <: Product] {
      type Out <: View
    }
    
    object ViewOf {
      transparent inline given mkViewOf[M <: Product]: ViewOf[M] = ${givenImpl[M]}
    
      @experimental // because .newClass is @experimental
      def givenImpl[M <: Product : Type](using Quotes): Expr[ViewOf[M]] = {
        import quotes.reflect.*
    
        def makeRefinement(parent: TypeRepr, namesAndTypes: List[(String, TypeRepr)]): TypeRepr =
          namesAndTypes.foldLeft(parent) { case (acc, (name, tpe)) => Refinement(acc, name, tpe) }
    
        def mkNamesAndTypes[mels: Type, mets: Type]: List[(String, TypeRepr)] =
          (Type.of[mels], Type.of[mets]) match {
            case ('[EmptyTuple], '[EmptyTuple]) => Nil
            case ('[mel *: melTail], '[met *: metTail] ) => {
              val name = Type.valueOfConstant[mel].get.toString
              val name1 = name + "1"
              val methodType = MethodType(List("args"))(_ => List(AnnotatedType(TypeRepr.of[Any], '{new scala.annotation.internal.Repeated()}.asTerm)), _ => TypeRepr.of[met])
              val methodType1 = ByNameType(TypeRepr.of[met])
              (name, methodType) :: (name1, methodType1) :: mkNamesAndTypes[melTail, metTail]
            }
          }
    
        val namesAndTypes = Expr.summon[Mirror.ProductOf[M]].get match {
          case '{ $m: Mirror.ProductOf[M] { type MirroredElemLabels = mels; type MirroredElemTypes = mets } } =>
            mkNamesAndTypes[mels, mets]
        }
    
        val refinement = makeRefinement(TypeRepr.of[View], namesAndTypes)
    
        def newType(cls: Symbol, name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags, privateWithin: Symbol = Symbol.noSymbol): Symbol = {
          given dotty.tools.dotc.core.Contexts.Context =
            quotes.asInstanceOf[scala.quoted.runtime.impl.QuotesImpl].ctx
          import dotty.tools.dotc.core.Decorators.toTypeName
          dotty.tools.dotc.core.Symbols.newSymbol(
            cls.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol],
            name.toTypeName,
            flags.asInstanceOf[dotty.tools.dotc.core.Flags.FlagSet],
            tpe.asInstanceOf[dotty.tools.dotc.core.Types.Type],
            privateWithin.asInstanceOf[dotty.tools.dotc.core.Symbols.Symbol]
          ).asInstanceOf[Symbol]
        }
    
        val viewOfImplDecls = (cls: Symbol) => List(newType(cls, "Out",
          TypeBounds(refinement, refinement),
          Flags.Override))
    
        val viewOfTypeTree = TypeTree.of[ViewOf[M]]
        val viewOfImplParents = List(TypeTree.of[AnyRef], viewOfTypeTree)
    
        val viewOfImplCls = Symbol.newClass(Symbol.spliceOwner, "ViewOfImpl", viewOfImplParents.map(_.tpe), viewOfImplDecls, selfType = None)
        val outSymb = viewOfImplCls.declaredType("Out").head
    
        val outTypeDef = TypeDef(outSymb)
        val viewOfImplClsDef = ClassDef(viewOfImplCls, viewOfImplParents, body = List(outTypeDef))
          // this would be an extra type ascription to be avoided
        // val newViewOfImpl = Typed(Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil), TypeTree.of[ViewOf[M]])
        val newViewOfImpl = Apply(Select(New(TypeIdent(viewOfImplCls)), viewOfImplCls.primaryConstructor), Nil)
    
        val res = Block(List(viewOfImplClsDef), newViewOfImpl).asExprOf[ViewOf[M]]
        println(res.show + "=" + res.asTerm.show(using Printer.TreeStructure))
        res
      }
    }