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