Michiel T
Michiel T

Reputation: 302

Usage of Guards in Scala Traits and Anonymous Classes

Let's assume we have the following hierarchy; a supertype has many DTO-like implementations. These implementations must always be valid according to some domain specific rules, therefore guard clauses in the form of assert-statements are added to the implemented bodies;

trait AbstractModel {
  def a: Int
  def b: Int
}

case class ConcreteModel(a: Int, b: Int, c: Int) extends AbstractModel {
  assert(a >= 0)
  assert(a > b)
  assert(c != 0)
}

Since the first and second guard statement must be true in our domain for áll implementations of the supertype, we'd rather move these checks to this supertype;

trait AbstractModelWithGuard {
  def a: Int
  def b: Int
  assert(a >= 0, s"Invalid attribute [$a >= 0]")
  assert(a > b, s"Invalid attribute [$a > $b]")
}

case class ConcreteModelVariant(a: Int, b: Int, c: Int) extends AbstractModel {
  assert(c != 0)
}

And all is well. Then we implement the supertype using an anonymous class (the trait is not sealed);

val x = new AbstractModelWithGuard {
  override val a: Int = 1
  override val b: Int = -1
}

Which results in a java.lang.AssertionError exception!

java.lang.AssertionError: assertion failed: Invalid attribute [0 > 0]

I'm assuming the assert-statements in my trait are evaluated in some intermediate instance of my anonymous object, before assignment. I didn't expect this behavior.

Why are my assert-statements failing and what is the recommended way to use guards in abstract classes in Scala?

Upvotes: 0

Views: 380

Answers (2)

Alexey Romanov
Alexey Romanov

Reputation: 170745

Pedrofurla's answer already provides the workarounds, but I'd prefer to explain the reason in more detail than "complicated initialization order". Especially since it isn't at all specific to traits:

class AbstractModelWithGuard {
  val a: Int = 11
  assert(a > 10, "`a` is not bigger than 10")
}

val x = new AbstractModelWithGuard { override val a = 11 } 
// throws the same AssertionError

Point 1: (concrete) val members correspond to 2 "real" members in JVM terms: a private field and a getter method. It's the method which overrides the supertype's def (or val in the above example). Fields can't override or be overridden.

Point 2: expressions in body of trait/class, and val/var initializers form the constructor. So your assertions are part of AbstractModelWithGuard's constructor, and initialization of a is in the anonymous class' constructor. In the example above, the private field of AbstractModelWithGuard also gets initialized in its constructor, but the method a in the assertion is the overriding one, so it will access the anonymous class' field instead!

Point 3: constructors of supertypes (both class and trait) are executed first. So the assertion is executed before the subclass' fields can be initialized.

Upvotes: 2

pedrofurla
pedrofurla

Reputation: 12783

Instance of traits have complicated initialization order:

scala> trait AbstractModelWithGuard {
     |   def a: Int
     |   assert(a > 10, "`a` is not bigger than 10")
     | }

scala> val x = new AbstractModelWithGuard { val a = 11 } 
java.lang.AssertionError: assertion failed: `a` is not bigger than 10
  at scala.Predef$.assert(Predef.scala:170)

One work around is the early initialization syntax:

scala> val x = new { val a = 11 } with AbstractModelWithGuard
x: AbstractModelWithGuard{val a: Int} = $anon$1@3bfef1ea

scala> x.a
res1: Int = 11

The other one, lazy vals:

scala> val x = new AbstractModelWithGuard { lazy val a = 11 } 
x: AbstractModelWithGuard{lazy val a: Int} = $anon$1@35e8b5e4

scala> x.a
res2: Int = 11

Upvotes: 2

Related Questions