Scala trait
s should define def
s, because
“A val can override a def, but a def cannot override a val”
(via Alvin Alexander via StackOverflow).
But it’s interesting to look at how the compiler treats that. Alvin Alexander did that, but only for Scala 2.12.8. Let’s have a look at 2.12, 2.13 and the just released 3.0 (formerly known as dotty).
Getting the scalac versions
You can install different versions of the Scala compiler via brew:
$ brew install scala@2.12
$ brew install scala # currently for 2.13
$ brew install dotty # currently Scala 3.0.0-RC2
Note: you can only have one
scala
andscalac
on your path, so you want to keep the versions you don’t use unlinked (or “keg-only” in brew-speak).
You can access the versions via
$ /usr/local/Cellar/scala@2.12/2.12.13/bin/scalac -version # one dash only!
Scala compiler version 2.12.13 -- Copyright 2002-2020, LAMP/EPFL and Lightbend, Inc.
$ /usr/local/Cellar/scala/2.13.5/bin/scalac --version
Scala compiler version 2.13.5 -- Copyright 2002-2020, LAMP/EPFL and Lightbend, Inc.
$ /usr/local/Cellar/dotty/3.0.0-RC3/bin/scalac --version
Scala compiler version 3.0.0-RC3 -- Copyright 2002-2021, LAMP/EPFL
At the end of scalac
As you might know, the compiler works in multiple steps (“phases”)., and it can output the result after each step. The -Xshow-phases
option prints out an overview of the compile-phases:

Here they are written out, if you want to read through them:
Scala 2.12.13
$ /usr/local/Cellar/scala@2.12/2.12.13/bin/scalac -Xshow-phases
phase name id description
---------- -- -----------
parser 1 parse source into ASTs, perform simple desugaring
namer 2 resolve names, attach symbols to named trees
packageobjects 3 load package objects
typer 4 the meat and potatoes: type the trees
patmat 5 translate match expressions
superaccessors 6 add super accessors in traits and nested classes
extmethods 7 add extension methods for inline classes
pickler 8 serialize symbol tables
refchecks 9 reference/override checking, translate nested objects
uncurry 10 uncurry, translate function values to anonymous classes
fields 11 synthesize accessors and fields, add bitmaps for lazy vals
tailcalls 12 replace tail calls by jumps
specialize 13 @specialized-driven class and method specialization
explicitouter 14 this refs to outer pointers
erasure 15 erase types, add interfaces for traits
posterasure 16 clean up erased inline classes
lambdalift 17 move nested functions to top level
constructors 18 move field definitions into constructors
flatten 19 eliminate inner classes
mixin 20 mixin composition
cleanup 21 platform-specific cleanups, generate reflective calls
delambdafy 22 remove lambdas
jvm 23 generate JVM bytecode
terminal 24 the last phase during a compilation run
Scala 2.13.5
/usr/local/Cellar/scala/2.13.5/bin/scalac -Xshow-phases
phase name id description
---------- -- -----------
parser 1 parse source into ASTs, perform simple desugaring
namer 2 resolve names, attach symbols to named trees
packageobjects 3 load package objects
typer 4 the meat and potatoes: type the trees
superaccessors 5 add super accessors in traits and nested classes
extmethods 6 add extension methods for inline classes
pickler 7 serialize symbol tables
refchecks 8 reference/override checking, translate nested objects
patmat 9 translate match expressions
uncurry 10 uncurry, translate function values to anonymous classes
fields 11 synthesize accessors and fields, add bitmaps for lazy vals
tailcalls 12 replace tail calls by jumps
specialize 13 @specialized-driven class and method specialization
explicitouter 14 this refs to outer pointers
erasure 15 erase types, add interfaces for traits
posterasure 16 clean up erased inline classes
lambdalift 17 move nested functions to top level
constructors 18 move field definitions into constructors
flatten 19 eliminate inner classes
mixin 20 mixin composition
cleanup 21 platform-specific cleanups, generate reflective calls
delambdafy 22 remove lambdas
jvm 23 generate JVM bytecode
terminal 24 the last phase during a compilation run
Scala 3.0.0-RC3 aka dotty
/usr/local/Cellar/dotty/3.0.0-RC3/bin/scalac -Xshow-phases
typer
inlinedPositions
sbt-deps
extractSemanticDB
posttyper
prepjsinterop
sbt-api
SetRootTree
pickler
inlining
postInlining
staging
pickleQuotes
{firstTransform, checkReentrant, elimPackagePrefixes, cookComments, checkStatic, betaReduce, inlineVals, expandSAMs, initChecker}
{elimRepeated, protectedAccessors, extmethods, uncacheGivenAliases, byNameClosures, hoistSuperArgs, specializeApplyMethods, refchecks}
{elimOpaque, tryCatchPatterns, patternMatcher, explicitJSClasses, explicitOuter, explicitSelf, elimByName, stringInterpolatorOpt}
{pruneErasedDefs, inlinePatterns, vcInlineMethods, seqLiterals, intercepted, getters, specializeFunctions, liftTry, collectNullableFields, elimOuterSelect, resolveSuper, functionXXLForwarders, paramForwarding, genericTuples, letOverApply, arrayConstructors}
erasure
{elimErasedValueType, pureStats, vcElideAllocations, arrayApply, addLocalJSFakeNews, elimPolyFunction, tailrec, completeJavaEnums, mixin, lazyVals, memoize, nonLocalReturns, capturedVars}
{constructors, instrumentation}
{lambdaLift, elimStaticThis, countOuterAccesses}
{dropOuterAccessors, flatten, renameLifted, transformWildcards, moveStatic, expandPrivate, restoreScopes, selectStatic, junitBootstrappers, collectSuperCalls, repeatableAnnotations}
genSJSIR
genBCode
Compile results
and the result is
$ /usr/local/Cellar/dotty/3.0.0-RC3/bin/scalac -Xprint:all Main.scala
[…]
result of Main.scala after MegaPhase{dropOuterAccessors, flatten, renameLifted, transformWildcards, moveStatic, expandPrivate, restoreScopes, selectStatic, collectSuperCalls, repeatableAnnotations}:
package <empty> {
@scala.annotation.internal.SourceFile("Main.scala") class MyClass extends
Object
, MyTrait {
def <init>(): Unit =
{
super()
this.id = 1
()
}
private val id: Int
def id(): Int = this.id
}
@scala.annotation.internal.SourceFile("Main.scala") trait MyTrait() extends
Object
{
def id(): Int
}
}
Example Code
I combined the function, abstract value and concrete value into one example:
trait MyTrait {
def id1: Int // function
val id2: Int // abstract value
val id3: Int = 3 // concrete value
}
class MyClass extends MyTrait {
val id1 = 1
val id2 = 2
override val id3 = 3
}
Results
Here are the results:

Let’s unpack this:
1. Scala 2.12 and 2.13 have the same output:
package <empty> {
abstract trait MyTrait extends Object {
<accessor> <sub_synth> protected[this] def MyTrait$_setter_$id3_=(x$1: Int): Unit;
def id1(): Int;
<stable> <accessor> def id2(): Int;
<stable> <accessor> <sub_synth> def id3(): Int;
def /*MyTrait*/$init$(): Unit = {
MyTrait.this.MyTrait$_setter_$id3_=((3: Int));
()
}
};
class MyClass extends Object with MyTrait {
override <accessor> protected[this] def MyTrait$_setter_$id3_=(x$1: Int): Unit = ();
private[this] val id1: Int = _;
<stable> <accessor> def id1(): Int = MyClass.this.id1;
private[this] val id2: Int = _;
<stable> <accessor> def id2(): Int = MyClass.this.id2;
private[this] val id3: Int = _;
override <stable> <accessor> def id3(): Int = MyClass.this.id3;
def <init>(): MyClass = {
MyClass.super.<init>();
MyClass.super./*MyTrait*/$init$();
MyClass.this.id1 = 1;
MyClass.this.id2 = 2;
MyClass.this.id3 = 3;
()
}
}
}
In the trait
, the function
def id1: Int
becomes
def id1(): Int;
the abstract val
val id2: Int
becomes
<stable> <accessor> def id2(): Int;
and the concrete val
val id3: Int = 3
needs the most work:
<stable> <accessor> <sub_synth> def id3(): Int;
<accessor> <sub_synth> protected[this] def MyTrait$_setter_$id3_=(x$1: Int): Unit;
def /*MyTrait*/$init$(): Unit = {
MyTrait.this.MyTrait$_setter_$id3_=((3: Int));
()
}
(The constructor would not be present if the trait
only contained id1
and id2
.)
What do <stable> <accessor> <sub_synth>
mean exactly? I have no idea. Neither does Google.
In the class
we have private val
s for all three:
private[this] val id1: Int = _;
<stable> <accessor> def id1(): Int = MyClass.this.id1;
private[this] val id2: Int = _;
<stable> <accessor> def id2(): Int = MyClass.this.id2;
private[this] val id3: Int = _;
override <stable> <accessor> def id3(): Int = MyClass.this.id3;
and the MyTrait$_setter_$id3_
from the trait
is overridden:
override <accessor> protected[this] def MyTrait$_setter_$id3_=(x$1: Int): Unit = ();
So in summary: So far, there’s not much of a difference.
2. Scala 3 has syntax-highlighting!
After the compile-phases-output was so ugly, it’s nice to see that the output of the phases is actually colored.
3. There’s an annotation
@scala.annotation.internal.SourceFile("Main.scala")
points to the source file. Neat.
4. <stable> <accessor> <sub_synth>
are gone
I guess now I’ll never find out what they were…
This also means that all fields in the trait
are now exactly the same:
def id1(): Int
def id2(): Int
def id3(): Int
and the setter-function for id3
goes from
<accessor> <sub_synth> protected[this] def MyTrait$_setter_$id3_=(x$1: Int): Unit;
to
def MyTrait$_setter_$id3_$eq(x$0: Int): Unit
In the class
, the three are also very much alike:
private val id1: Int
def id1(): Int = this.id1
private val id2: Int
def id2(): Int = this.id2
private var id3: Int
override def id3(): Int = this.id3
Bytecode deep-dive
Let’s take one step further and take the actual Bytecode apart, using javap -c
:

Observations:
- Scala 2.12 and 2.13 result in the exact same Bytecode
- Scala 3 places the constructor first
- The
MyTrait$_setter_$id3_$eq(int)
actually contains some setting-code now!
as compared topublic void MyTrait$_setter_$id3_$eq(int); Code: 0: aload_0 1: iload_1 2: putfield #25 // Field id3:I 5: return
public void MyTrait$_setter_$id3_$eq(int); Code: 0: return
- the
scala.runtime.Statics$#releaseFence()
has a single jvm-operation (invokestatic
) resulting from it.
Next Level: JVM 17
The compiler can also target a specific version of the Java platform, using the -target:8
(-Xtarget
for Scala 3) or -release
option. However, for this simple example, there were no differences in the bytecode.
Summary
So, what to make of this? Easy: Using def
or val
in traits is purely a developer ergonomics decision, not a runtime difference.