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
}
}
Selectable, macros and type refinement to allow you to generate types which kinda do what you want. IntelliJ doesn't support it yet, but Scala Metals would let you use type hints with types generated throughdataFrame.types[YourType].map(_.caseClassField)- but whether it's suitable for your case is hard to tell. Authors gave a talk about it last week in Scala conference in Warsaw but I am not sure when the videos will be available.Selectablethis might be worth investigating.type.