如何查看 Scala 用于自动生成 case 类 的 apply 函数的代码?
How can I view the code that Scala uses to automatically generate the apply function for case classes?
定义 Scala 案例 class 时,会自动生成一个应用函数,其行为类似于 java 中默认构造函数的行为方式。如何查看自动生成应用函数的代码?我假设代码是 Scala 编译器中某个地方的宏,但我不确定。
澄清一下,我对查看给定案例的结果应用方法不感兴趣 class 但对生成应用方法的 macro/code 感兴趣。
这不是宏。方法由编译器合成 "manually".
apply
、unapply
、copy
生成于scala.tools.nsc.typechecker.Namers
/** Given a case class
* case class C[Ts] (ps: Us)
* Add the following methods to toScope:
* 1. if case class is not abstract, add
* <synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
* 2. add a method
* <synthetic> <case> def unapply[Ts](x: C[Ts]) = <ret-val>
* where <ret-val> is the caseClassUnapplyReturnValue of class C (see UnApplies.scala)
*
* @param cdef is the class definition of the case class
* @param namer is the namer of the module class (the comp. obj)
*/
def addApplyUnapply(cdef: ClassDef, namer: Namer): Unit = {
if (!cdef.symbol.hasAbstractFlag)
namer.enterSyntheticSym(caseModuleApplyMeth(cdef))
val primaryConstructorArity = treeInfo.firstConstructorArgs(cdef.impl.body).size
if (primaryConstructorArity <= MaxTupleArity)
namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef))
}
def addCopyMethod(cdef: ClassDef, namer: Namer): Unit = {
caseClassCopyMeth(cdef) foreach namer.enterSyntheticSym
}
private def templateSig(templ: Template): Type = {
//...
// add apply and unapply methods to companion objects of case classes,
// unless they exist already; here, "clazz" is the module class
if (clazz.isModuleClass) {
clazz.attachments.get[ClassForCaseCompanionAttachment] foreach { cma =>
val cdef = cma.caseClass
assert(cdef.mods.isCase, "expected case class: "+ cdef)
addApplyUnapply(cdef, templateNamer)
}
}
// add the copy method to case classes; this needs to be done here, not in SyntheticMethods, because
// the namer phase must traverse this copy method to create default getters for its parameters.
// here, clazz is the ClassSymbol of the case class (not the module). (!clazz.hasModuleFlag) excludes
// the moduleClass symbol of the companion object when the companion is a "case object".
if (clazz.isCaseClass && !clazz.hasModuleFlag) {
val modClass = companionSymbolOf(clazz, context).moduleClass
modClass.attachments.get[ClassForCaseCompanionAttachment] foreach { cma =>
val cdef = cma.caseClass
def hasCopy = (decls containsName nme.copy) || parents.exists(_.member(nme.copy).exists)
// scala/bug#5956 needs (cdef.symbol == clazz): there can be multiple class symbols with the same name
if (cdef.symbol == clazz && !hasCopy)
addCopyMethod(cdef, templateNamer)
}
}
equals
、hashCode
、toString
生成于scala.tools.nsc.typechecker.SyntheticMethods
/** Synthetic method implementations for case classes and case objects.
*
* Added to all case classes/objects:
* def productArity: Int
* def productElement(n: Int): Any
* def productPrefix: String
* def productIterator: Iterator[Any]
*
* Selectively added to case classes/objects, unless a non-default
* implementation already exists:
* def equals(other: Any): Boolean
* def hashCode(): Int
* def canEqual(other: Any): Boolean
* def toString(): String
*
* Special handling:
* protected def writeReplace(): AnyRef
*/
trait SyntheticMethods extends ast.TreeDSL {
//...
访问器的符号在 scala.reflect.internal.Symbols
中创建
/** For a case class, the symbols of the accessor methods, one for each
* argument in the first parameter list of the primary constructor.
* The empty list for all other classes.
*
* This list will be sorted to correspond to the declaration order
* in the constructor parameter
*/
final def caseFieldAccessors: List[Symbol] = {
// We can't rely on the ordering of the case field accessors within decls --
// handling of non-public parameters seems to change the order (see scala/bug#7035.)
//
// Luckily, the constrParamAccessors are still sorted properly, so sort the field-accessors using them
// (need to undo name-mangling, including the sneaky trailing whitespace)
//
// The slightly more principled approach of using the paramss of the
// primary constructor leads to cycles in, for example, pos/t5084.scala.
val primaryNames = constrParamAccessors map (_.name.dropLocal)
def nameStartsWithOrigDollar(name: Name, prefix: Name) =
name.startsWith(prefix) && name.length > prefix.length + 1 && name.charAt(prefix.length) == '$'
caseFieldAccessorsUnsorted.sortBy { acc =>
primaryNames indexWhere { orig =>
(acc.name == orig) || nameStartsWithOrigDollar(acc.name, orig)
}
}
}
private final def caseFieldAccessorsUnsorted: List[Symbol] = info.decls.toList.filter(_.isCaseAccessorMethod)
也许我可以指出代码库中可能相关的几点。
首先,有一种方法可以将 Scala 语言规范 语法直接关联到源代码。例如,case classes 规则
TmplDef ::= ‘case’ ‘class’ ClassDef
有关
/** {{{
* TmplDef ::= [case] class ClassDef
* | [case] object ObjectDef
* | [override] trait TraitDef
* }}}
*/
def tmplDef(pos: Offset, mods: Modifiers): Tree = {
...
in.token match {
...
case CASECLASS =>
classDef(pos, (mods | Flags.CASE) withPosition (Flags.CASE, tokenRange(in.prev /*scanner skips on 'case' to 'class', thus take prev*/)))
...
}
}
规范继续
A case class definition of [tps](ps1)…(ps)
with type parameters
tps and value parameters ps implies the definition of a companion
object, which serves as an extractor object.
object {
def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs)
def unapply[tps](: [tps]) =
if (x eq null) scala.None
else scala.Some(.xs11,…,.xs1)
}
所以让我们尝试寻找隐含的定义
def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs)
这是合成定义的另一种说法。有希望地存在 MethodSynthesis.scala
/** Logic related to method synthesis which involves cooperation between
* Namer and Typer.
*/
trait MethodSynthesis {
因此我们又找到了两条潜在的线索Namer
和Typer
。我想知道里面有什么?但首先 MethodSynthesis.scala
只有大约 300 LOC,所以让我们浏览一下。我们偶然发现了 promising line
val methDef = factoryMeth(classDef.mods & (AccessFlags | FINAL) | METHOD | IMPLICIT | SYNTHETIC, classDef.name.toTermName, classDef)
"factoryMeth
"...有一个环。找用法!我们很快就被引导到
/** The apply method corresponding to a case class
*/
def caseModuleApplyMeth(cdef: ClassDef): DefDef = {
val inheritedMods = constrMods(cdef)
val mods =
if (applyShouldInheritAccess(inheritedMods))
(caseMods | (inheritedMods.flags & PRIVATE)).copy(privateWithin = inheritedMods.privateWithin)
else
caseMods
factoryMeth(mods, nme.apply, cdef)
}
看来我们走对了路。我们还注意到 name
nme.apply
也就是
val apply: NameType = nameType("apply")
急切地,我们找到了 caseModuleApplyMeth
的用法,我们被钻进了 Namer.addApplyUnapply
/** Given a case class
* case class C[Ts] (ps: Us)
* Add the following methods to toScope:
* 1. if case class is not abstract, add
* <synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
* 2. add a method
* <synthetic> <case> def unapply[Ts](x: C[Ts]) = <ret-val>
* where <ret-val> is the caseClassUnapplyReturnValue of class C (see UnApplies.scala)
*
* @param cdef is the class definition of the case class
* @param namer is the namer of the module class (the comp. obj)
*/
def addApplyUnapply(cdef: ClassDef, namer: Namer): Unit = {
if (!cdef.symbol.hasAbstractFlag)
namer.enterSyntheticSym(caseModuleApplyMeth(cdef))
val primaryConstructorArity = treeInfo.firstConstructorArgs(cdef.impl.body).size
if (primaryConstructorArity <= MaxTupleArity)
namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef))
}
哇哦!文档说明
<synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
这看起来与 SLS 版本非常相似
def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs)
我们在黑暗中的跌跌撞撞似乎让我们有了发现。
我注意到,虽然其他人发布了生成方法 name 的代码片段,signature,type,符号table中的相应符号,以及几乎所有其他内容,到目前为止还没有人发布生成actual body[=的代码片段案例 class 伴随对象 apply
方法的 48=]。
该代码在scala.tools.nsc.typechecker.Unapplies.factoryMeth(mods: Global.Modifiers, name: Global.TermName, cdef: Global.ClassDef): Global.DefDef
which is defined in src/compiler/scala/tools/nsc/typechecker/Unapplies.scala
中,相关部分是这样的:
atPos(cdef.pos.focus)(
DefDef(mods, name, tparams, cparamss, classtpe,
New(classtpe, mmap(cparamss)(gen.paramToArg)))
)
它使用 TreeDSL
内部领域特定语言在抽象语法树中生成语法节点,并且(大致)意味着:
- 在树中的当前位置 (
atPos(cdef.pos.focus)
)
- 在方法定义节点中拼接(
DefDef
)
- 其主体只是一个
New
节点,即构造函数调用。
TreeDSL
特征状态的描述:
The goal is that the code generating code should look a lot like the code it generates.
我认为这是真的,即使您不熟悉编译器的内部结构,代码也很容易阅读。
再次比较generating代码与generated代码:
DefDef(mods, name, tparams, cparamss, classtpe,
New(classtpe, mmap(cparamss)(gen.paramToArg)))
def apply[Tparams](constructorParams): CaseClassType =
new CaseClassType(constructorParams)
定义 Scala 案例 class 时,会自动生成一个应用函数,其行为类似于 java 中默认构造函数的行为方式。如何查看自动生成应用函数的代码?我假设代码是 Scala 编译器中某个地方的宏,但我不确定。
澄清一下,我对查看给定案例的结果应用方法不感兴趣 class 但对生成应用方法的 macro/code 感兴趣。
这不是宏。方法由编译器合成 "manually".
apply
、unapply
、copy
生成于scala.tools.nsc.typechecker.Namers
/** Given a case class
* case class C[Ts] (ps: Us)
* Add the following methods to toScope:
* 1. if case class is not abstract, add
* <synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
* 2. add a method
* <synthetic> <case> def unapply[Ts](x: C[Ts]) = <ret-val>
* where <ret-val> is the caseClassUnapplyReturnValue of class C (see UnApplies.scala)
*
* @param cdef is the class definition of the case class
* @param namer is the namer of the module class (the comp. obj)
*/
def addApplyUnapply(cdef: ClassDef, namer: Namer): Unit = {
if (!cdef.symbol.hasAbstractFlag)
namer.enterSyntheticSym(caseModuleApplyMeth(cdef))
val primaryConstructorArity = treeInfo.firstConstructorArgs(cdef.impl.body).size
if (primaryConstructorArity <= MaxTupleArity)
namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef))
}
def addCopyMethod(cdef: ClassDef, namer: Namer): Unit = {
caseClassCopyMeth(cdef) foreach namer.enterSyntheticSym
}
private def templateSig(templ: Template): Type = {
//...
// add apply and unapply methods to companion objects of case classes,
// unless they exist already; here, "clazz" is the module class
if (clazz.isModuleClass) {
clazz.attachments.get[ClassForCaseCompanionAttachment] foreach { cma =>
val cdef = cma.caseClass
assert(cdef.mods.isCase, "expected case class: "+ cdef)
addApplyUnapply(cdef, templateNamer)
}
}
// add the copy method to case classes; this needs to be done here, not in SyntheticMethods, because
// the namer phase must traverse this copy method to create default getters for its parameters.
// here, clazz is the ClassSymbol of the case class (not the module). (!clazz.hasModuleFlag) excludes
// the moduleClass symbol of the companion object when the companion is a "case object".
if (clazz.isCaseClass && !clazz.hasModuleFlag) {
val modClass = companionSymbolOf(clazz, context).moduleClass
modClass.attachments.get[ClassForCaseCompanionAttachment] foreach { cma =>
val cdef = cma.caseClass
def hasCopy = (decls containsName nme.copy) || parents.exists(_.member(nme.copy).exists)
// scala/bug#5956 needs (cdef.symbol == clazz): there can be multiple class symbols with the same name
if (cdef.symbol == clazz && !hasCopy)
addCopyMethod(cdef, templateNamer)
}
}
equals
、hashCode
、toString
生成于scala.tools.nsc.typechecker.SyntheticMethods
/** Synthetic method implementations for case classes and case objects.
*
* Added to all case classes/objects:
* def productArity: Int
* def productElement(n: Int): Any
* def productPrefix: String
* def productIterator: Iterator[Any]
*
* Selectively added to case classes/objects, unless a non-default
* implementation already exists:
* def equals(other: Any): Boolean
* def hashCode(): Int
* def canEqual(other: Any): Boolean
* def toString(): String
*
* Special handling:
* protected def writeReplace(): AnyRef
*/
trait SyntheticMethods extends ast.TreeDSL {
//...
访问器的符号在 scala.reflect.internal.Symbols
/** For a case class, the symbols of the accessor methods, one for each
* argument in the first parameter list of the primary constructor.
* The empty list for all other classes.
*
* This list will be sorted to correspond to the declaration order
* in the constructor parameter
*/
final def caseFieldAccessors: List[Symbol] = {
// We can't rely on the ordering of the case field accessors within decls --
// handling of non-public parameters seems to change the order (see scala/bug#7035.)
//
// Luckily, the constrParamAccessors are still sorted properly, so sort the field-accessors using them
// (need to undo name-mangling, including the sneaky trailing whitespace)
//
// The slightly more principled approach of using the paramss of the
// primary constructor leads to cycles in, for example, pos/t5084.scala.
val primaryNames = constrParamAccessors map (_.name.dropLocal)
def nameStartsWithOrigDollar(name: Name, prefix: Name) =
name.startsWith(prefix) && name.length > prefix.length + 1 && name.charAt(prefix.length) == '$'
caseFieldAccessorsUnsorted.sortBy { acc =>
primaryNames indexWhere { orig =>
(acc.name == orig) || nameStartsWithOrigDollar(acc.name, orig)
}
}
}
private final def caseFieldAccessorsUnsorted: List[Symbol] = info.decls.toList.filter(_.isCaseAccessorMethod)
也许我可以指出代码库中可能相关的几点。
首先,有一种方法可以将 Scala 语言规范 语法直接关联到源代码。例如,case classes 规则
TmplDef ::= ‘case’ ‘class’ ClassDef
有关
/** {{{
* TmplDef ::= [case] class ClassDef
* | [case] object ObjectDef
* | [override] trait TraitDef
* }}}
*/
def tmplDef(pos: Offset, mods: Modifiers): Tree = {
...
in.token match {
...
case CASECLASS =>
classDef(pos, (mods | Flags.CASE) withPosition (Flags.CASE, tokenRange(in.prev /*scanner skips on 'case' to 'class', thus take prev*/)))
...
}
}
规范继续
A case class definition of
[tps](ps1)…(ps)
with type parameters tps and value parameters ps implies the definition of a companion object, which serves as an extractor object.object { def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs) def unapply[tps](: [tps]) = if (x eq null) scala.None else scala.Some(.xs11,…,.xs1) }
所以让我们尝试寻找隐含的定义
def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs)
这是合成定义的另一种说法。有希望地存在 MethodSynthesis.scala
/** Logic related to method synthesis which involves cooperation between
* Namer and Typer.
*/
trait MethodSynthesis {
因此我们又找到了两条潜在的线索Namer
和Typer
。我想知道里面有什么?但首先 MethodSynthesis.scala
只有大约 300 LOC,所以让我们浏览一下。我们偶然发现了 promising line
val methDef = factoryMeth(classDef.mods & (AccessFlags | FINAL) | METHOD | IMPLICIT | SYNTHETIC, classDef.name.toTermName, classDef)
"factoryMeth
"...有一个环。找用法!我们很快就被引导到
/** The apply method corresponding to a case class
*/
def caseModuleApplyMeth(cdef: ClassDef): DefDef = {
val inheritedMods = constrMods(cdef)
val mods =
if (applyShouldInheritAccess(inheritedMods))
(caseMods | (inheritedMods.flags & PRIVATE)).copy(privateWithin = inheritedMods.privateWithin)
else
caseMods
factoryMeth(mods, nme.apply, cdef)
}
看来我们走对了路。我们还注意到 name
nme.apply
也就是
val apply: NameType = nameType("apply")
急切地,我们找到了 caseModuleApplyMeth
的用法,我们被钻进了 Namer.addApplyUnapply
/** Given a case class
* case class C[Ts] (ps: Us)
* Add the following methods to toScope:
* 1. if case class is not abstract, add
* <synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
* 2. add a method
* <synthetic> <case> def unapply[Ts](x: C[Ts]) = <ret-val>
* where <ret-val> is the caseClassUnapplyReturnValue of class C (see UnApplies.scala)
*
* @param cdef is the class definition of the case class
* @param namer is the namer of the module class (the comp. obj)
*/
def addApplyUnapply(cdef: ClassDef, namer: Namer): Unit = {
if (!cdef.symbol.hasAbstractFlag)
namer.enterSyntheticSym(caseModuleApplyMeth(cdef))
val primaryConstructorArity = treeInfo.firstConstructorArgs(cdef.impl.body).size
if (primaryConstructorArity <= MaxTupleArity)
namer.enterSyntheticSym(caseModuleUnapplyMeth(cdef))
}
哇哦!文档说明
<synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)
这看起来与 SLS 版本非常相似
def apply[tps](ps1)…(ps): [tps] = new [Ts](xs1)…(xs)
我们在黑暗中的跌跌撞撞似乎让我们有了发现。
我注意到,虽然其他人发布了生成方法 name 的代码片段,signature,type,符号table中的相应符号,以及几乎所有其他内容,到目前为止还没有人发布生成actual body[=的代码片段案例 class 伴随对象 apply
方法的 48=]。
该代码在scala.tools.nsc.typechecker.Unapplies.factoryMeth(mods: Global.Modifiers, name: Global.TermName, cdef: Global.ClassDef): Global.DefDef
which is defined in src/compiler/scala/tools/nsc/typechecker/Unapplies.scala
中,相关部分是这样的:
atPos(cdef.pos.focus)( DefDef(mods, name, tparams, cparamss, classtpe, New(classtpe, mmap(cparamss)(gen.paramToArg))) )
它使用 TreeDSL
内部领域特定语言在抽象语法树中生成语法节点,并且(大致)意味着:
- 在树中的当前位置 (
atPos(cdef.pos.focus)
) - 在方法定义节点中拼接(
DefDef
) - 其主体只是一个
New
节点,即构造函数调用。
TreeDSL
特征状态的描述:
The goal is that the code generating code should look a lot like the code it generates.
我认为这是真的,即使您不熟悉编译器的内部结构,代码也很容易阅读。
再次比较generating代码与generated代码:
DefDef(mods, name, tparams, cparamss, classtpe, New(classtpe, mmap(cparamss)(gen.paramToArg)))
def apply[Tparams](constructorParams): CaseClassType =
new CaseClassType(constructorParams)