Search code examples
scalamacroscase-classscala-compiler

How can I view the code that Scala uses to automatically generate the apply function for case classes?


When defining a Scala case class, an apply function is automatically generated which behaves similarly to the way the default constructor in java behaves. How can I see the code which automatically generates the apply function? I presume the code is a macro in the Scala compiler somewhere but I'm not sure.

To clarify I am not interested in viewing the resultant apply method of a given case class but interested in the macro/code which generates the apply method.


Solution

  • It's not a macro. Methods are synthesized by compiler "manually".

    apply, unapply, copy are generated in 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)
        }
      }
    

    equals, hashCode, toString are generated in 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 {
    //...
    

    Symbols for accessors are created in 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)