【问题标题】:How can I view the code that Scala uses to automatically generate the apply function for case classes?如何查看 Scala 用于为案例类自动生成 apply 函数的代码?
【发布时间】:2020-04-02 19:09:10
【问题描述】:

在定义 Scala 案例类时,会自动生成一个 apply 函数,该函数的行为类似于 java 中默认构造函数的行为方式。如何查看自动生成 apply 函数的代码?我认为代码是某个地方的 Scala 编译器中的宏,但我不确定。

澄清一下,我对查看给定案例类的结果应用方法不感兴趣,但对生成应用方法的宏/代码感兴趣。

【问题讨论】:

  • 我猜这不是宏,因为宏是在案例类之后很久才添加到 Scala 中的。
  • 它不是宏,它只是编译器的功能。为什么要查看该代码?
  • @LuisMiguelMejíaSuárez 我只是好奇而已。

标签: scala macros case-class scala-compiler


【解决方案1】:

这不是宏。方法由编译器“手动”合成。

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

equalshashCodetoStringscala.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)

【讨论】:

    【解决方案2】:

    也许我可以指出代码库中可能相关的几点。

    首先,有一种方法可以将 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*/)))
            ...
          }
        }
    

    规范继续

    带有类型参数的?[tps](ps1)…(ps?) 的案例类定义 tps 和值参数 ps 暗示了同伴的定义 对象,用作提取器对象。

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

    我们在黑暗中的绊脚石似乎让我们有了一个发现。

    【讨论】:

      【解决方案3】:

      我注意到,虽然其他人已经发布了生成方法的名称签名类型的代码片段,符号表中的相应符号,以及几乎所有其他内容,到目前为止,没有人发布生成案例类伴随对象 apply 方法的 实际主体 的代码。

      该代码在scala.tools.nsc.typechecker.Unapplies.factoryMeth(mods: Global.Modifiers, name: Global.TermName, cdef: Global.ClassDef): Global.DefDef 中,在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 trait 的描述如下:

      目标是生成代码的代码应该看起来很像它生成的代码。

      我认为这是真的,即使您不熟悉编译器内部结构,也可以使代码易于阅读。

      再次比较生成ing代码与生成ed代码:

      DefDef(mods, name, tparams, cparamss, classtpe,
       New(classtpe, mmap(cparamss)(gen.paramToArg)))
      
      def apply[Tparams](constructorParams): CaseClassType =
        new CaseClassType(constructorParams)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-12-05
        • 1970-01-01
        • 2014-07-20
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-12-13
        相关资源
        最近更新 更多