Scala: How to define properties in an abstract base class or trait

This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 4.13, “How to define properties in an abstract base class or trait.”

Problem

Using Scala, you want to define abstract or concrete properties in an abstract base class (or trait) that can be referenced in all child classes.

Solution

You can declare both val and var fields in an abstract Scala class (or trait), and those fields can be abstract or have concrete implementations. All of these variations are shown in this recipe.

Abstract val and var fields

The following example demonstrates an Animal trait with abstract val and var fields, along with a simple concrete method named sayHello, and an override of the toString method:

abstract class Pet (name: String) {
    val greeting: String
    var age: Int
    def sayHello { println(greeting) }
    override def toString = s"I say $greeting, and I'm $age"
}

The following Dog and Cat classes extend the Animal class and provide values for the greeting and age fields. Notice that the fields are again specified as val or var:

class Dog (name: String) extends Pet (name) {
    val greeting = "Woof"
    var age = 2
}

class Cat (name: String) extends Pet (name) {
    val greeting = "Meow"
    var age = 5
}

The functionality can be demonstrated with a simple driver object:

object AbstractFieldsDemo extends App {
    val dog = new Dog("Fido")
    val cat = new Cat("Morris")
    dog.sayHello
    cat.sayHello
    println(dog)
    println(cat)
    // verify that the age can be changed
    cat.age = 10
    println(cat)
}

The resulting output looks like this:

Woof
Meow
I say Woof, and I'm 2
I say Meow, and I'm 5
I say Meow, and I'm 10

Concrete field implementations are presented in the Discussion, because it helps to understand how the Scala compiler translates your code in the preceding examples.

Discussion

As shown, you can declare abstract fields in an abstract class as either val or var, depending on your needs. The way abstract fields work in abstract classes (or traits) is interesting:

  • An abstract var field results in getter and setter methods being generated for the field.
  • An abstract val field results in a getter method being generated for the field.
  • When you define an abstract field in an abstract class or trait, the Scala compiler does not create a field in the resulting code; it only generates the methods that correspond to the val or var field.

In the example shown in the Solution, if you look at the code that’s created by scalac using the -Xprint:all option, or by decompiling the resulting Pet.class file, you won’t find greeting or age fields. For instance, if you decompile the class, the output shows only methods in the class, no fields:

import scala.*;
import scala.runtime.BoxesRunTime;

public abstract class Pet
{
    public abstract String greeting();
    public abstract int age();
    public abstract void age_$eq(int i);
    public void sayHello() {
        Predef$.MODULE$.println(greeting());
    }
    public String toString(){
        // code omitted
    }
    public Pet(String name){}
}

Because of this, when you provide concrete values for these fields in your concrete classes, you must again define your fields to be val or var. Because the fields don’t actually exist in the abstract base class (or trait), the override keyword is not necessary.

As another result of this, you may see developers define a def that takes no parameters in the abstract base class rather than defining a val. They can then define a val in the concrete class, if desired. This technique is demonstrated in the following code:

abstract class Pet (name: String) {
    def greeting: String
}

class Dog (name: String) extends Pet (name) {
    val greeting = "Woof"
}

object Test extends App {
    val dog = new Dog("Fido")
    println(dog.greeting)
}

Given this background, it’s time to examine the use of concrete val and var fields in abstract classes.

Concrete val fields in abstract classes

When defining a concrete val field in an abstract class, you can provide an initial value, and then override that value in concrete subclasses:

abstract class Animal {
    val greeting = "Hello"                // provide an initial value
    def sayHello { println(greeting) }
    def run
}

class Dog extends Animal {
    override val greeting = "Woof"        // override the value
    def run { println("Dog is running") }
}

In this example, the greeting variable is created in both classes. To demonstrate this, running the following code:

abstract class Animal {
    val greeting = { println("Animal"); "Hello" }
}

class Dog extends Animal {
    override val greeting = { println("Dog"); "Woof" }
}

object Test extends App {
    new Dog
}

results in this output, showing that both values are created:

Animal
Dog

To prove this, you can also decompile both the Animal and Dog classes, where you’ll find the greeting declared like this:

private final String greeting = "Hello";

To prevent a concrete val field in an abstract base class from being overridden in a subclass, declare the field as a final val:

abstract class Animal {
    final val greeting = "Hello"   // made the field 'final'
}

class Dog extends Animal {
    val greeting = "Woof"          // this line won't compile
}

Concrete var fields in abstract classes

You can also give var fields an initial value in your trait or abstract class, and then refer to them in your concrete subclasses, like this:

abstract class Animal {
    var greeting = "Hello"
    var age = 0
    override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
    greeting = "Woof"
    age = 2
}

In this case, these fields are declared and assigned in the abstract base class, as shown in the decompiled code for the Animal class:

private String greeting;
private int age;
public Animal(){
    greeting = "Hello";
    age = 0;
}
// more code ...

Because the fields are declared and initialized in the abstract Animal base class, there’s no need to redeclare the fields as val or var in the concrete Dog subclass.

You can verify this by looking at the code the Scala compiler generates for the Dog class. When you compile the code with scalac -Xprint:all, and look at the last lines of output, you’ll see how the compiler has converted the Dog class:

class Dog extends Animal {
  def <init>(): Dog = {
    Dog.super.<init>();
    Dog.this.greeting_=("Woof");
    Dog.this.age_=(2);
    ()
  }
}

Because the fields are concrete fields in the abstract base class, they just need to be reassigned in the concrete Dog class.

Don’t use null

As discussed in many recipes in this book, including Recipe 20.5, “Eliminate null Values from Your Code”, you shouldn’t use null values in these situations. If you’re tempted to use a null, instead initialize the fields using the Option/Some/None pattern. The following example demonstrates how to initialize val and var fields with this approach:

trait Animal {
    val greeting: Option[String]
    var age: Option[Int] = None
    override def toString = s"I say $greeting, and I'm $age years old."
}

class Dog extends Animal {
    val greeting = Some("Woof")
    age = Some(2)
}

object Test extends App {
    val d = new Dog
    println(d)
}

Running this Test object yields the following output:

I say Some(Woof), and I'm Some(2) years old.

See Also