In a recent training course I was asked if Scala supports static or dynamic polymorphism. These are not terms I had heard before, so I had some homework to do. A bit of research showed this terminology refers to the same thing as early and late binding, which I'm more familiar with. So, here we have a quick discussion of early binding (or static polymorphism) and late binding (or dynamic polymorphism), and how it relates to Scala's polymorphic methods and overloading.
For this discussion we'll need to be precise about our terminology surrounding types. We're going to use the term type in the way used in type theory. It refers to a constraint on a value known at compile-time. In this view types only exist at compile-time.
Programmers sometimes refer to run-time or dynamic types. This is information available at runtime about a value. We're going to use the term tags to refer to these. The tag may sometimes contain the same information as a type, but sometimes it will not. For example, in the following code the type is Option[Int]
but the tag is Some
.
val anOption: Option[Int] = Some(1)
Given class definitions like
class Foo() {
def doIt: String =
"Foo"
}
class Bar() extends Foo() {
override def doIt: String =
"Bar"
}
what method is chosen when we write
val foo: Foo = new Bar()
foo.doIt
There are two possibilities:
Foo
) is used to choose the method so foo.doIt
evaluates to "Foo"
; orBar
) is used to choose the method so foo.doIt
evaluates to "Bar"
.The former choice is early binding (static polymorphism), while the latter is late binding (dynamic polymorphism). Scala, like Java and most other OO languages, uses late binding. The above code will evaluate to "Bar"
.
We've seen that the tag of the method receiver (the object on which we call the method) is involved in determining which method is used. We might also ask if the method parameters are also involved in making that decision. Take the following code.
class Foo() {
def doEet(foo: Foo): String =
"FooFoo"
def doEet(foo: Bar): String =
"FooBar"
}
class Bar() extends Foo() {
}
Here we have two overloaded variants of doEet
. What is the result of the following expressions?
val aFoo: Foo = new Foo()
val foo: Foo = new Bar()
val bar: Bar = new Bar()
aFoo.doEet(foo)
aFoo.doEet(bar)
The expression aFoo.doEet(foo)
evaluates to "FooFoo"
, while aFoo.doEet(bar)
evaluates to "FooBar"
. This shows that the type, not the tag, of method parameters is used to choose between overloaded methods.
Therefore we can say that Scala has late binding for the method receiver (the object on which we call the method) but early binding for method parameters (to resolve overloading).
It's worth noting that some languages use the tags of all method parameters to choose between implementations. This is known as multiple dispatch. The choice made in Scala, where only a single tag is used, is known as single dispatch.
Multiple dispatch is very uncommon. It has a few issues:
Multiple dispatch is mainly found in the Lisp family of languages. CLOS, the standard object system for Common Lisp, features multiple dispatch, for example.
It's possible to run into confusing situations using overloading. Take the following code for example.
object Foo {
def add(numbers: List[Int]): Double =
numbers.foldLeft(0.0){ (elt, accum) => elt.toDouble + accum }
def add(numbers: List[Double]): Double =
numbers.foldLeft(0.0){ (elt, accum) => elt + accum }
}
It seems simple but it doesn't compile, failing with an error
error: double definition:
def add(numbers: List[Int]): Double at line 2 and
def add(numbers: List[Double]): Double at line 5
have same type after erasure: (numbers: List)Double
def add(numbers: List[Double]): Double =
^
This is due to a JVM limitation around the representation of generic types. (In fact there are good arguments in favour of type erasure, but it is beside the point here.) There is no doubt Scala has its fair share of goofiness, a lot of which comes from constraints imposed by the JVM. The Scala Puzzlers website describes many other corner cases.
The good news is it's easy to stay away from the goofiness. What we teach and what we use in our coding are a few patterns that lead to simple and comprehendable code. In the above example I'd try to represent the common features of Int
and Double
using a type class. This has clear semantics, stays well away from issues with overloading, and is far more flexible to boot.