Here’s a way to look at this that might be helpful. In static languages, the language comes with a system for assigning types to all expressions and if a program doesn’t follow the rules that allow it to do that, it doesn’t type check. In dynamic languages, there’s no such rule: instead of expressions having types, values have types and they just flow through the program. This is why static type theory people will sometimes describe dynamic languages as “unityped” because by their notion of what a type is, all expressions in a dynamic language have the type Any
. Of course, dynamic language people think this is dumb because to them that’s not what a type is—it’s a classification of a value, not an expression.
Dynamic dispatch in static object-oriented languages like C++ and D is about allowing a little bit of that dynamic “values not expressions” behavior in an otherwise static language: if A
has subtypes B
and C
and you have code where the type of an expression is A
then when the program is running, the value that actually appears there can be of type B
or C
(let’s assume that A
is strictly abstract). Now let’s say you have a variable a
which has static type A
, and then you see a.m()
. Which m
method gets called? If the dispatch is dynamic, then the method choice will depend on the actual runtime type of a
: B.m
will be called if a
is an instance of type B
; C.m
will be called if a
is an instance of type C
.
If later in the code you see f(a)
then this is not a method call but a static function call (let’s ignore unified function call syntax for the sake of simplicity, the concept also applies to non-virtual methods, so the syntax is kind of beside the point). What this means is that it doesn’t matter what the actual type of a
is—the function that will be called is determined based on the static type of a
, which is just A
. So the function signature that will be called will be f(a::A)
even if there is a function signature f(b::B)
and a
is an instance of type B
(and likewise for C
, of course).
How does this play out in Julia? There simply is no concept of the “static type” of any variable or expression. Yes, you can write a::A
in a method signature or function body but all that does is ensure that a
is an instance of type A
, either by dispatch or type assertion. The only type associated with a
is still it’s actual runtime type. You can test this quite easily by mocking up the above situation:
abstract type A end
struct B <: A end
struct C <: A end
f(a::A) = "it's an A"
f(b::B) = "it's a B"
f(c::C) = "it's a C"
g(a::A) = f(a)
In action:
julia> g(B())
"it's a B"
julia> g(C())
"it's a C"
If Julia’s dispatch were actually static function overloading, then the method of f
which is called from g
would be determined based on the static type of a
which would be A
and both of these would return "it's an A"
, but that’s not what happens. Instead, the method is determined based on the actual type of a
just as in dynamic single dispatch in class-based object-oriented languages.
Note that this difference is independent of when the compiler happens to be able to figure out what method needs to be called—it’s a visible behavioral difference. Static dispatch means that the method f(::A)
must be called whereas dynamic dispatch means that the appropriate f(::B)
or f(::C)
method must be called. Julia takes a hard stand on this: there is not even a concept of “the static type” of anything in the language, nor is there any way to choose static dispatch when calling a function.