Contrast the static type (at compile-time) versus type at runtime of variable aa in
val aa: Fruit = new Apple
| |
static type runtime type
The key is to understand the compiler is not aware of types at runtime and only verifies constraints specified by static types. Because you explicitly instructed the compiler with type annotation :Fruit in aa: Fruit the compiler does not know aa is actually an Apple at runtime. Hence aa.apple is a compiler error because Fruit does not have the field apple.
One use-case of inheritance is to enable subtype polymorphism:
trait Shape {
def area: Double
}
class Circle(val radius: Float) extends Shape {
override def area: Double = radius * radius * Math.PI
}
class Rectangle(val width: Double, val height: Double) extends Shape {
override def area: Double = width * height
}
object ShapeArea {
def area(shape: Shape): Double = shape.area // <=== define method once for all kinds of shape
}
import ShapeArea._
area(new Circle(2)) // res0: Double = 12.566370614359172
area(new Rectangle(2, 3)) // res1: Double = 6.0
As a side-note, in idiomatic functional programming Scala there exists an alternative ad-hoc polymorphism (typeclass) approach:
sealed trait Shape
case class Circle(radius: Float) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
trait Area[T <: Shape] {
def area(shape: T): Double
}
object Area {
def area[T <: Shape](shape: T)(implicit ev: Area[T]): Double = ev.area(shape) // <== define method once for all kinds of shape
implicit val circleArea: Area[Circle] =
(circle) => circle.radius * circle.radius * Math.PI
implicit val rectangleArea: Area[Rectangle] =
(rectangle) => rectangle.height * rectangle.width
}
import Area._
area(Circle(2)) // res0: Double = 12.566370614359172
area(Rectangle(2, 3)) // res1: Double = 6.0
appleis safe. This is basic typing.