Home > Software design >  Scala3: Crafting Types Through Metaprogramming?
Scala3: Crafting Types Through Metaprogramming?

Time:11-27

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 !

CodePudding user response:

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 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
  }
}
  • Related