To be fair, it’s right in the Types page of the manual and it’s not made clear there either. It somewhat assumes readers have a vague familiarity with OOP virtual functions and various languages’ distinction or lack thereof between primitives and classes, which is totally unnecessary to understand how Julia is organized and plausibly confuses first-language programmers.
The silly short answer is types can be checked at runtime, but that’s not really an explanation. Strictly speaking, Java is a statically typed language, but it does have runtime types on the semantic level as well. You can paste this example in an online Java compiler to see the printout, but the comments say it all. If the syntax and bit of boilerplate is too unfamiliar, you can skip to the explanation after the MWE.
public class Main {
public static void main(String[] args) {
Animal a = new Cat(); // static Animal, runtime Cat
// eat() does static dispatch, speak() does dynamic dispatch
a.eat(); // "Animals eat food."
a.speak(); // "Meow!"
a = new Dog(); // still static Animal, now runtime Dog
a.eat(); // "Animals eat food."
a.speak(); // "Bark!"
}
}
class Animal {
static void eat() { System.out.println("Animals eat food."); }
void speak() { System.out.println("Animals speak."); }
}
class Cat extends Animal {
static void eat() { System.out.println("Cats eat meat."); }
@Override
void speak() { System.out.println("Meow!"); }
}
class Dog extends Animal {
static void eat() { System.out.println("Dogs eat meat and more."); }
@Override
void speak() { System.out.println("Bark!"); }
}
Animal a
declares a variable a
with a type Animal
. Statically typed languages require every expression to be associated with a type before runtime, sometimes declared, sometimes inferred from other expressions. In most implementations of statically typed languages, a distinct compilation phase would verify static types and dispatch static method calls like a.eat()
.
At runtime however, a
is first assigned to Cat()
, not a direct Animal
instance. That may not look much different from the Animal
declaration, but the Cat()
call doesn’t impose anything on the variable a
; later, we assign a = Dog()
, and while it doesn’t happen, we could assign a = Animal()
. The method a.speak()
is defined to dispatch over the runtime type, so Cat()
meows and Dog()
barks. When these method calls are dispatched depends on implementation. In this simple case, a compiler can actually infer that a
is first a Cat()
then a Dog()
, thus statically dispatch their respective speak()
calls, a kind of devirtualization. In more complex cases or if the compiler decides devirtualization isn’t worth it, the dispatch occurs at runtime.
That’s not nearly the entire story, especially across languages, but it should give you a good idea of the classic distinction of a variable’s static type versus its instances’ runtime types. The Manual’s Types section’s paragraphs you’re quoting appears to have been written to ease people from those languages into Julia, but again, it’s really not necessary to understand Julia because the language only has runtime types and dispatches method calls on them, despite the type annotations sometimes resembling those in statically typed languages. As explained already, type stability is only a practice to leverage the compiler implementation when we need to, not static type semantics. Despite juliac’s trim option being fully AOT compiled and disallowing unlimited dynamic dispatch, it will still be a dynamically typed language because the compiler can fully handle type unstable code in limited circumstances.
I’d argue the Manual isn’t a great place for addressing the practical misconceptions like “static semantics fast, dynamic semantics slow” because it generally specifies the language and thus avoids talking about implementation, despite the latter’s clear influence on the former. It does occasionally mention the compiler with the understanding this is an implementation detail, like the Methods page describing compile-time dispatch, and it might be better to draw a brief parallel there to other languages’ compilers also inferring runtime types to devirtualize calls instead of talking about other language’s designs this much. Or perhaps link a separate unstable section dedicated to describing the reference implementation and good practices for performance, like a souped-up Performance Tips centralizing some optimizations mentioned in the Developer Documentation. I don’t pretend to know how best to communicate with both first-language programmers and people coming from other languages, just my 2 cents.