Lazy Enforcement of Requirements on Abstract Fields


#1

Dear all,

I have a base trait

trait T1 {val v: Int; require(v > 0)}

which needs to enforce that v > 0 to all classes deriving from it. The scheme above, however, doesn’t work because v is already initialised (to 0) when the overridden value for v is provided:

scala> val t1 = new T1{val v = 14}
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:212)
  at T1$class.$init$(<console>:14)
  ... 33 elided

Choosing v to be lazy doesn’t help either because, then, it needs to be initialised but no default value makes sense for v in the context of my work. How would I go about enforcing the requirement then?

There is a reason, in fact, why I don’t make T1 a class. That’s because, my “real” T1 also has abstract functions and passing those when extending T1 is such a disgust.

TIA,
–Hossein


#2

This should work

val t1 = new {val v = 14} with T1


#3

You can also define v as a def. This avoids issues with the order of
initialisation.


#4

I tried that before the other solution:
trait T1 {def v: Int; require(v > 0)}

defined trait T1
val t1 = new T1{val v = 14}
java.lang.IllegalArgumentException: requirement failed
at scala.Predef$.require(Test.sc:260)
at #worksheet#.T1.$init$(Test.sc:1)
at #worksheet#.$anon$1.(Test.sc:3)
at #worksheet#.t1$lzycompute(Test.sc:3)
at #worksheet#.t1(Test.sc:3)
at #worksheet#.#worksheet#(Test.sc:3)


#5

You have to declare it as a def in the implementation as well.

val t1 = new T1 { def v = 14 }

#6

OK, looks like my presentation was an oversimplification. Here is a more realistic version:

scala> trait B1 {val v: Int; require(v > 0)}
defined trait B1

scala> trait B2 extends {val v = 10; def f(): Int} with B1
<console>:1: error: only concrete field definitions allowed in early object initialization section
trait B2 extends {val v = 10; def f(): Int} with B1
                                  ^

Obviously, I cannot turn B2 into a class because it has abstract stuff of its own. What now?


#7

Yes, Hosam. That seems to do the trick. Thanks. :slight_smile:

scala> trait B1 {def v: Int; require(v > 0)}
defined trait B1

scala> trait B2 extends B1 {def v = 10; def f(): Int}
defined trait B2

Now, would you mind explaining to me what difference between val and def is helping here?


#8

valis a value that must be instantiated at the start of the application (unless lazy). def is only evaluated when it is executed (no initialization). It will allays execute even when returning the same value.

When require needs to check the value it executes the function v that returns the value. Since v is abstract in B1 I assume it will only be called when B2 is instantiated.

Note: I don’t think the requirement in your example above is actually checked. You have to instantiate B2. Try using the vale -10.


#9

Thank you for the comprehensive explanation. I have new questions now:

Start of the application? If I understand you correctly, you’re implying there is no real late-binding for the abstract vals. Otherwise, the initialisation should have been postponed to the time when a concrete value is provided further down the inheritance hierarchy. Or, am I missing anything?

Well, it indeed is checked:

scala> trait B3 extends B1 {def v = -10; def f(): Int}
defined trait B3

scala> val b3 = new B3 {def f() = 124000}
java.lang.IllegalArgumentException: requirement failed
  at scala.Predef$.require(Predef.scala:212)
  at B1$class.$init$(<console>:17)
  ... 33 elided

Albeit, I would say it should have been checked earlier — making B3 illegal even before its instantiation.

Cheers,
–Hossein


#10

Take what I say with a grain of salt. Hopefully someone will correct me when I err.

That is wrong. That is should be when the instance is created.

That is what I was implying, but only within the traits “constructor”. But to create a B1 instance we still need to define a v in the instance. To test this “late binding”:

trait B1 {val v: Int; println(v) }
defined trait B1

val b1 = new B1 { val v = 100; }

0
b1: B1 = $anon$1@195d4780

Notice how the print (part of the the constructor) shows a 0 and not a 100. For the B1 trait, the val v does not act as if it is abstract.

On the other hand when you declare a def v,

trait B1 {def v: Int; println(v) }
defined trait B1

val b1 = new B1 { def v = 100; }

100
b1: B1 = $anon$1@6cceb281

In this case def v is in fact abstract and needs to be defined somewhere before it can be accessed. Hence the “late binding” (I put this in quotes because I think this is all done at compile time).

Of course, due to referential transparency, you can also do the following:

trait B1 {def v: Int; println(v) }
defined trait B1

val b1 = new B1 { val v = 100; }

0
b1: B1{val v: Int} = $anon$1@29188050

But again, we revert to the initial example.

I confess this is not intuitive for me but I am sure these behaviors exist to make the implementations coherent and correct. Maybe someone can explain how and why.

This may not be doable. Note that the stacking the traits in a given order determines what constructor are used and in what order. The require is simply part of the constructor that is only executed during run-time.

I do recall someone asking for these kind of checks during compile-time. There were some suggestions.

Hope some of this makes sense.


#11

This is a sad proof of the abstract vals being inaccurately named abstract in Scala. The compiler already enforces some default value for them, making them (quietly) concrete, as opposed to abstract. Such a val of a trait is imperfectly like a pure virtual (in its C++ sense) for the trait's user needs to (also) provide a concrete value for the val before instantiating the trait. The imperfection comes from their lack of virtual dispatch or late binding. It’s too late now for a change of name, but, they should have rather been called something like “apparently uninitialised.”

Oh, yes. I now recall that require is simply a wrapper around an exception. And, exceptions comes to play at runtime. Yet, in this very case, v's value is know at compile-time. So, one can expect employing a different creature than require to statically outlaw B3.


#12

This is a sad proof of the abstract vals being inaccurately named abstract in Scala.

That’s not exactly true. You need to play around with the order of initialization, though:

val t1 = new { val v = 14 } with T1

This means that the value of v is initialised before the constructor of T1 is evaluated.

The compiler already enforces some default value for them, making them (quietly) concrete, as opposed to abstract.

Not exactly. You asked the compiler to allocate memory for an object that contains a val. It did that, and the JVM calls memset(0) to initialise the whole instance of B1 before running its constructor. This is why uninitialised fields in a Java class are set to null or 0. Note that you never instantiated T1 so we can’t say that the compiler made it concrete.

The imperfection comes from their lack of virtual dispatch or late binding.

Yet, in this very case, v's value is known at compile-time.

Consider what your definition translates into.

trait T1 { val v: Int; require(v > 0) }
class C extends T1 { val v = 14 }

Is rougly equivalent to the following code in Java:

abstract class T1 {
  abstract int v();
  T1() {
    require(v() > 0);
  }
}

class C extends T1 {
  private int _v;
  final int v() { return _v; }
  C() {
    super();
    _v = 14;
  }
}

Note that the call to super() happens before the initialisation. This is the normal order of execution: the constructor of the parent class is executed before that of the child class. As you can see, the call to require happens before the new value is set.


#13

Every val has a backing field and class initialization order is known as Scala linearization order (you can google that). There always need to be some initialization order, so at some point during initialization some fields will be initialized and some will not. You need to know the initialization order to avoid running into uninitialized fields. Alternatively (and that’s the better option IMO) you can move any logic from constructor to factory method (like apply method on a companion object).

If you want to have a backing field (so the value will be memoized) but still override the initializer then you can do it separately, i.e.:

trait T1 {
  val v = makeV
  require(v > 0)
  protected def makeV: Int
}
class C extends T1 {
  override protected def makeV: Int = 10
}