Isaac Flaum
Isaac Flaum

Reputation: 55

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.

Upvotes: 2

Views: 797

Answers (3)

Jörg W Mittag
Jörg W Mittag

Reputation: 369536

I noticed that, while others have posted the pieces of code that generate the name of the method, the signature, the type, the corresponding symbols in the symbol table, and pretty much everything else, so far nobody has posted the piece of code that generates the actual body of the case class companion object apply method.

That code is in 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, and the relevant part is this:

atPos(cdef.pos.focus)(
 DefDef(mods, name, tparams, cparamss, classtpe,
   New(classtpe, mmap(cparamss)(gen.paramToArg)))
)

which uses the TreeDSL internal Domain Specific Language for generating Syntax Nodes in the Abstract Syntax Tree, and (roughly) means this:

  • At the current position in the tree (atPos(cdef.pos.focus))
  • Splice in a method definition node (DefDef)
  • Whose body is just a New node, i.e. a constructor invocation.

The description of the TreeDSL trait states:

The goal is that the code generating code should look a lot like the code it generates.

And I think that is true, and makes the code easy to read even if you are not familiar with the compiler internals.

Compare the generating code once again with the generated code:

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

Upvotes: 2

Mario Galic
Mario Galic

Reputation: 48420

Perhaps I could point out few points in the codebase that might be relevant.

First, there is a way to correlate Scala Language Specification grammar directly to source code. For example, case classes rule

TmplDef  ::=  β€˜case’ β€˜class’ ClassDef

relates to 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*/)))
        ...
      }
    }

Specification continues

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π‘˜) 
} 

so let us try to hunt for implied definition of

def apply[tps](ps1)…(ps𝑛): 𝑐[tps] = new 𝑐[Ts](xs1)…(xs𝑛)

which is another way of saying synthesised definition. Promisingly, there exists MethodSynthesis.scala

/** Logic related to method synthesis which involves cooperation between
 *  Namer and Typer.
 */
trait MethodSynthesis {

Thus we find two more potential clues Namer and Typer. I wonder what is in there? But first MethodSynthesis.scala has only approx 300 LOC, so let us just skim through a bit. We stumble accross a promising line

val methDef = factoryMeth(classDef.mods & (AccessFlags | FINAL) | METHOD | IMPLICIT | SYNTHETIC, classDef.name.toTermName, classDef)

"factoryMeth"... there is a ring to it. Find usages! We are quickly led to

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

It seems we are on the right track. We also note the name

nme.apply

which is

val apply: NameType                = nameType("apply")

Eagerly, we find usages of caseModuleApplyMeth and we are wormholed to 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))
    }

Woohoo! The documentation states

<synthetic> <case> def apply[Ts](ps: Us): C[Ts] = new C[Ts](ps)

which seems eerily similar to SLS version

def apply[tps](ps1)…(ps𝑛): 𝑐[tps] = new 𝑐[Ts](xs1)…(xs𝑛)

Our stumbling-in-the-dark seems to have led us to a discovery.

Upvotes: 2

Dmytro Mitin
Dmytro Mitin

Reputation: 51683

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)

Upvotes: 3

Related Questions