Reputation: 302
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
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
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