104

I was going through the effective scala slides and it mentions on slide 10 to never use val in a trait for abstract members and use def instead. The slide does not mention in detail why using abstract val in a trait is an anti-pattern. I would appreciate it if someone can explain best practice around using val vs def in a trait for abstract methods

Martin Senne
  • 5,939
  • 6
  • 30
  • 47
Mansur Ashraf
  • 1,337
  • 3
  • 9
  • 12

4 Answers4

138

A def can be implemented by either of a def, a val, a lazy val or an object. So it's the most abstract form of defining a member. Since traits are usually abstract interfaces, saying you want a val is saying how the implementation should do. If you ask for a val, an implementing class cannot use a def.

A val is needed only if you need a stable identifier, e.g. for a path-dependent type. That's something you usually don't need.


Compare:

trait Foo { def bar: Int }

object F1 extends Foo { def bar = util.Random.nextInt(33) } // ok

class F2(val bar: Int) extends Foo // ok

object F3 extends Foo {
  lazy val bar = { // ok
    Thread.sleep(5000)  // really heavy number crunching
    42
  }
}

If you had

trait Foo { val bar: Int }

you wouldn't be able to define F1 or F3.


Ok, and to confuse you and answer @om-nom-nom—using abstract vals can cause initialisation problems:

trait Foo { 
  val bar: Int 
  val schoko = bar + bar
}

object Fail extends Foo {
  val bar = 33
}

Fail.schoko  // zero!!

This is an ugly problem which in my personal opinion should go away in future Scala versions by fixing it in the compiler, but yes, currently this is also a reason why one should not use abstract vals.

Edit (Jan 2016): You are allowed to override an abstract val declaration with a lazy val implementation, so that would also prevent the initialisation failure.

0__
  • 66,707
  • 21
  • 171
  • 266
  • 8
    words about tricky initialization order and surprising nulls? – om-nom-nom Oct 28 '13 at 18:26
  • Yeah... I would't even go there. True these are also arguments against val, but I think the basic motivation should just be to hide implementation. – 0__ Oct 28 '13 at 18:28
  • 2
    This may have changed in a recent Scala version (2.11.4 as of this comment), but you can override a `val` with a `lazy val`. Your assertion that you wouldn't be able to create `F3` if `bar` was a `val` is not correct. That said, abstract members in traits should always be `def`'s – mplis Nov 10 '14 at 19:31
  • The Foo/Fail example works as expected if you replace `val schoko = bar + bar` with `lazy val schoko = bar + bar`. That's one way of having some control over the initialization order. Also, using `lazy val` instead of `def` in the derived class avoids recomputation. – Adrian Jan 28 '16 at 00:34
  • @om-nom-nom , Could you please elaborate more on initialisation problem or null issue with using val in trait? Any links are much appreciated! – Khoa Feb 10 '17 at 07:27
  • @BlueSky well, it's explained a bit in above answer body (check out edit starting with _using abstract vals can cause initialisation problems_), are you eager for more? – om-nom-nom Feb 13 '17 at 10:00
  • 2
    If you change `val bar: Int` to `def bar: Int` `Fail.schoko` is still zero. – Jasper-M May 17 '17 at 13:24
  • Yes, or you could even have non-abstract `def` or `val` which is overridden by a `val` or a `def` which accesses a `val` and run into the same problem. The problem here is using a non-final member during initialization at all, and whether you declare it as `def` or a `val` is a red herring. – Alexey Romanov May 18 '17 at 10:54
  • happened to me and glad that i find this edit in this post, abstract int vals were being zero exactly it is described here – Deliganli Aug 11 '17 at 10:04
  • To me this says you shouldn't define a `val` in a trait that references other members. Changing `val bar: Int` in `trait Foo` doesn't solve the initialization problem. In the `trait Foo` example, it's fine to have `val bar: Int` but it isn't fine to have `val schoko = bar + bar` regardless of how `bar` is defined. – Jono Aug 22 '18 at 18:50
8

I prefer not use val in traits because the val declaration has unclear and non-intuitive order of initialization. You may add a trait to already working hierarchy and it would break all things that worked before, see my topic: why using plain val in non-final classes

You should keep all things about using this val declarations in mind which eventually road you to an error.


Update with more complicated example

But there are times when you could not avoid using val. As @0__ had mentioned sometimes you need a stable identifier and def is not one.

I would provide an example to show what he was talking about:

trait Holder {
  type Inner
  val init : Inner
}
class Access(val holder : Holder) {
  val access : holder.Inner =
    holder.init
}
trait Access2 {
  def holder : Holder
  def access : holder.Inner =
    holder.init
}

This code produces the error:

 StableIdentifier.scala:14: error: stable identifier required, but Access2.this.holder found.
    def access : holder.Inner =

If you take a minute to think you would understand that compiler has a reason to complain. In the Access2.access case it could not derive return type by any means. def holder means that it could be implemented in broad way. It could return different holders for each call and that holders would incorporate different Inner types. But Java virtual machine expects the same type to be returned.

Community
  • 1
  • 1
ayvango
  • 5,867
  • 3
  • 34
  • 73
  • 3
    Order of initialization shouldn't matter, but instead we get surprising NPE's during runtime, vis-a-vis anti-pattern. – Coder Guy Dec 18 '14 at 04:36
  • scala has declarative syntax that hide imperative nature behind. Sometimes that imperativeness works counter-intuitive – ayvango Dec 18 '14 at 09:23
0

I agree with the other answers about avoiding abstract vals for the reason that it provides more options to implementations.

There are cases where you might need them:

  • For a path-dependent type (as mentioned by @0__).
  • Where implementations might be expensive and it is used in a concrete def.
  • (Are there others? If so please comment and I'll add them in).

The more important things to know is when it is safe to override something with a val and to have a lazy val that does not override something.


Rule 1: Never override a val or def with a non-lazy val unless it is a constructor parameter:

trait TraitWithVal {
  // It makes no difference if this is concrete or abstract.
  val a: String
  val b: String = a
}

class OverrideValWithVal extends TraitWithVal {
  // Bad: b will be null.
  override val a: String = "a"
}

class OverrideValWithLazyVal extends TraitWithVal {
  // Ok: b will be "a".
  override lazy val a: String = "a"
}

// Ok: b will be "a".
class OverrideValWithConstructorVal(override val a: String = "a") extends TraitWithVal

//class OverrideValWithDef extends TraitWithVal {
//  // Compilation error: method a needs to be a stable, immutable value.
//  override def a: String = "a"
//}

println((new OverrideValWithVal).b) // null
println((new OverrideValWithLazyVal).b) // a
println((new OverrideValWithConstructorVal).b) // a

The same rule applies to a def:

trait TraitWithDef {
  // It makes no difference if this is concrete or abstract.
  def a: String
  val b: String = a
}

class OverrideDefWithVal extends TraitWithDef {
  // Bad: b will be null.
  override val a: String = "a"
}

class OverrideDefWithLazyVal extends TraitWithDef {
  // Ok: b will be "a".
  override lazy val a: String = "a"
}

// Ok: b will be "a".
class OverrideDefWithConstructorVal(override val a: String = "a") extends TraitWithDef

class OverrideDefWithDef extends TraitWithDef {
  // Ok: b will be "a".
  override def a: String = "a"
}

println((new OverrideDefWithVal).b) // null
println((new OverrideDefWithLazyVal).b) // a
println((new OverrideDefWithConstructorVal).b) // a
println((new OverrideDefWithDef).b) // a

You might be wondering whether it would be ok to override a val with another val so long as it isn't used during initialisation. There is at least one edge cases which break this:

trait TraitWithValAndLazyVal {
  val a: String = "A"
  def b: String = a
}

class OverrideLazyValWithVal extends TraitWithValAndLazyVal {
  // Bad: This on its own is ok but not if it is indirectly referenced during initialisation and overridden.
  override val a = "a"
  val c = b
}

class OverrideValWithVal extends OverrideLazyValWithVal {
  override val a = "a"
}

println((new OverrideValWithVal).a) // a
println((new OverrideValWithVal).b) // a
println((new OverrideValWithVal).c) // null

Given that we already apply this rule to overriding defs then this makes using vals a little more acceptable in my opinion.

If you use a linter to enforce the override keyword and make sure your code never has any override val definitions then you are good.

You might be able to allow final override val but it's possible there are other edge cases that I haven't thought of.


Rule 2: Never use a lazy val that is not overriding another lazy val or def.

As far as I can tell there also is no good reason to have a lazy val that isn't overriding something. All the examples that I can come up with where it is needed, it is needed only because it violates Rule 1 and exposes the edge case I described earlier.

For example:

trait NormalLookingTrait {
  def a: String
  val b: String = a
}

trait TraitWithAbstractVal extends NormalLookingTrait {
  val c: String
}

class OverrideValWithVal extends TraitWithAbstractVal {
  override def a: String = c
  override val c = "a"
}

println((new OverrideValWithVal).a) // a
println((new OverrideValWithVal).b) // null
println((new OverrideValWithVal).c) // a

So we make b a lazy val:

trait SuspiciousLookingTrait2 {
  def a: String
  lazy val b: String = a
}

trait TraitWithAbstractVal2 extends SuspiciousLookingTrait2 {
  val c: String
}

class OverrideValWithVal2 extends TraitWithAbstractVal2 {
  override def a: String = c
  override val c = "a"
}

println((new OverrideValWithVal2).a) // a
println((new OverrideValWithVal2).b) // a
println((new OverrideValWithVal2).c) // a

Looks ok, except when we go one step further:

trait SuspiciousLookingTrait2 {
  def a: String
  lazy val b: String = a
}

trait TraitWithAbstractVal2 extends SuspiciousLookingTrait2 {
  val c: String
}

class OverrideValWithVal2 extends TraitWithAbstractVal2 {
  override def a: String = c
  override val c = "a"
  val d = b
}

class OverrideValWithVal3 extends OverrideValWithVal2 {
  override val c = "a"
}

println((new OverrideValWithVal3).a) // a
println((new OverrideValWithVal3).b) // null
println((new OverrideValWithVal3).c) // a
println((new OverrideValWithVal3).d) // null

I now get what people mean when they say to only use lazy when it is absolutely necessary and never for delayed initialisation.

It's probably safe to break this rule if the trait / class is final but even that smells fishy.

steinybot
  • 5,491
  • 6
  • 37
  • 55
  • I just realised that rule 1 also applies to classes with concrete `val`s which means that if a classes uses another `val` anywhere in it's initialisation then the referenced `val` has to be final or risk `null`s when extended. – steinybot May 11 '21 at 02:56
-4

Always using def seems a bit awkward since something like this won't work:

trait Entity { def id:Int}

object Table { 
  def create(e:Entity) = {e.id = 1 }  
}

You will get the following error:

error: value id_= is not a member of Entity
Dimitry
  • 4,503
  • 6
  • 26
  • 40
  • 2
    No relevant. You have an error too if you use val instead of def (error: reassignment to val), and that's perfectly logical. – volia17 Jun 16 '15 at 09:33
  • Not if you use `var`. The point is, if the they are fields they should be designated as such. I just think having everything as `def` is short sighted. – Dimitry Jun 16 '15 at 19:56
  • @Dimitry, sure, using `var` let's you break encapsulation. But using a `def` (or a `val`) is preferred over a global variable. I think what you're looking for is something like `case class ConcreteEntity(override val id: Int) extends Entity` so that you can create it from `def create(e: Entity) = ConcreteEntity(1)` This is safer than breaking the encapsulation and allowing any class to change Entity. – Jono Aug 22 '18 at 18:56