如何查看 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".

applyunapplycopy生成于scala.tools.nsc.typechecker.Namers

https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/typechecker/Namers.scala#L1839-L1862

/** 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
}

https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/typechecker/Namers.scala#L1195-L1219

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)
    }
  }

equalshashCodetoString生成于scala.tools.nsc.typechecker.SyntheticMethods

https://github.com/scala/scala/blob/2.13.x/src/compiler/scala/tools/nsc/typechecker/SyntheticMethods.scala

/** 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

中创建

https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/internal/Symbols.scala#L2103-L2128

/** 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

Parser.tmplDef

有关
    /** {{{
     *  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 {

因此我们又找到了两条潜在的线索NamerTyper。我想知道里面有什么?但首先 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 的代码片段,signaturetype,符号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)