Where the verb lives: left vs right of the data
This is a language point, not parallelism yet — but it’s what will later make threads and the GPU elegant.
▶ Notebook (demo + solution): Multiple dispatch vs OOP
⬅ Previous: 1 — Interpreted vs compiled · Next: 3 — Threads & concurrency ➡
The same idea — “the area of a shape” — written two ways:
| call | the verb is… | |
|---|---|---|
| OOP (Python) | shape.area() |
on the right, after the dot |
| dispatch (Julia) | area(shape) |
on the left, prefix |
area is a generic function, living outside the types.The receiver left of the dot decides the OOP method. The verb on the left decides the Julia method — from all its arguments.
class Shape:
def area(self): ...
class Circle(Shape):
def __init__(self, r): self.r = r
def area(self): return 3.14159 * self.r**2
class Rectangle(Shape):
def __init__(self, L, w): self.L, self.w = L, w
def area(self): return self.L * self.w
Circle(2.0).area() # dispatch on the object (left of the dot)perimeter) means editing every class. → invasiveSeparate the data (types) from the operations (functions):
areais external to the types. The version that runs is picked from the argument type.
The verb is on the left → it can look at every argument, not only the first.
(Circle, Rectangle) — that’s multiple dispatch.a.collide(b) only ever looks at a. The second argument gets no say.Add a new operation on existing types — without touching them:
Add a new type — without touching existing code:
| add a type | add an operation | |
|---|---|---|
| OOP | easy (new class) | edit every class |
| dispatch | easy (new struct) | easy (new function) |
The “expression problem”: OOP makes new types cheap but new operations invasive. Dispatch makes both cheap — on the extensibility axis.
This isn’t only elegant design — it’s the mechanism behind the compilation module’s specialization.
area(Circle(2.0)): Julia sees a Circle, picks the method, compiles a specialized native version. Same story as f(3.0) vs f(3).| method resolved… | |
|---|---|
Python obj.method() |
at run time, by lookup, every call — unspecialized |
Julia area(x) |
at compile time — if the type is inferable — chosen, inlined, lookup gone |
Warning
Not inferable → Julia dispatches at run time, exactly like Python. Our own area.([Circle, Rectangle, Circle]) builds a Vector{Shape}: measured ~10× slower than Vector{Circle}. Concrete types buy the speed.
Multiple dispatch + JIT + inferable types = functions that are generic and fast.
| Idea | Python (OOP) | Julia (dispatch) |
|---|---|---|
| Where the method lives | inside the object (obj.method()) |
outside the types, generic function |
| Chosen from | the type of obj (one — singledispatch too) |
the types of all arguments |
| Add a type | easy | easy |
| Add an operation | edit every class (or monkey-patch) | add a function, types untouched |
| Two-argument dispatch | isinstance ladder / Visitor |
just another method |
| Link to speed | — | the engine of specialization — if types are inferable |
Later, the same
*will pick the GPU version all by itself — just because its arguments areCuArray. Same code, different hardware (the memory/GPU module).
methods(area) lists every method Julia knows for a generic function.⬅ Previous: 1 — Interpreted vs compiled · Next: 3 — Threads & concurrency ➡