Mikel San Vicente
Mikel San Vicente

Reputation: 3863

Scala trait inheritance strange behavior

Can someone explain why is this program printing a 0 instead of 4?

trait Rectangular {
  def width: Int
  def height: Int
  val area = width * height
}
case class Square(val size: Int) extends Rectangular {
  val width = size
  val height = size
}

print(Square(2).area)

but it works when I make the vals lazy

trait Rectangular {
  def width: Int
  def height: Int
  val area = width * height
}
case class Square(val size: Int) extends Rectangular {
  lazy val width = size
  lazy val height = size
}

print(Square(2).area)

Upvotes: 1

Views: 102

Answers (2)

puhlen
puhlen

Reputation: 8529

The problem is the val in your trait gets initialized before the vals from your class.

So when you call the constructor, first area gets initialized. It sets its value as width * height. Neither width nor height have been initialized yet so their value is zero. This means area = 0 * 0 = 0.

After that, width and height get set to the value of size, but it's too late for area at this point.

It works with lazy val because lazy vals check to see if they have been initialized when accessed, and initialize themselves if not. A regular val has no such checks.

Upvotes: 1

Silvio Mayolo
Silvio Mayolo

Reputation: 70267

This is an unfortunate issue with the way Scala constructs val members.

trait Rectangular {
  def width: Int
  def height: Int
  val area = width * height
}
case class Square(val size: Int) extends Rectangular {
  val width = size
  val height = size
}

Here, internally, Scala makes private members in Square called width and height. It initializes them to zero. Then, in the Square constructor, it sets them. Basically, it does something close to this Java code.

public abstract class Rectangular {
    private int area;
    public Rectangular() {
        area = width() * height();
    }
    public abstract int width();
    public abstract int height();
    public int area() { return area; }
}
public class Square extends Rectangular {
    private int width, height;
    public Square(int size) {
        Rectangular();
        width = size;
        height = size;
    }
    public int width() { return width; }
    public int height() { return width; }
}

Note that the Rectangular constructor is called before the Square constructor, so area sees the default zero width and height values before they're ever set. As you've already discovered, using lazy val fixes the problem.

trait Rectangular {
  def width: Int
  def height: Int
  val area = width * height
}
case class Square(val size: Int) extends Rectangular {
  lazy val width = size
  lazy val height = size
}

Alternatively, you can use early initializer syntax to force the values to be written in the right order.

trait Rectangular {
  def width: Int
  def height: Int
  val area = width * height
}
case class Square(val size: Int) extends {
  val width = size
  val height = size
} with Rectangular

The lazy val solution is usually preferable, as it leads to fewer surprises down the road.

For more information, see the Scala FAQ about this particular topic.

Upvotes: 2

Related Questions