Julia's Multiple Dispatch Is an Extensibility Decision, Not Just a Speed Trick
Julia's multiple dispatch is an extensibility decision: new types can extend an existing generic function without editing it. Here's the worked example, plus the ambiguity and type-stability bill that comes with it.
18 Feb 2026, 06:49 UTC

Adding a new shape to a geometry library you didn't write is a small version of a large problem. In a class-based language, the library author has to have anticipated your type, or you subclass something and hope the library's methods call the right hook. Julia takes a different route: functions are generic, methods attach to them from anywhere, and the method is chosen from the tuple of argument types at the call site. That's multiple dispatch, and it's better understood as an extensibility decision than as a performance trick.
What actually happens when you call a function
In Julia, area isn't a function body; it's a generic function with a method table. Each definition like area(s::Circle) adds a row keyed by the declared argument types. At a call, Julia looks at the runtime types of the arguments, finds the most specific applicable method, and runs it.
Two consequences follow. First, you can add a method for your own type without editing the original module — the classic expression problem, solved by making dispatch open. Second, the compiler can often resolve the call statically when it can infer concrete argument types, which is where the speed reputation comes from.
A worked example: adding a triangle without touching the geometry module
Suppose a package defines this. It's a constructed illustration, not a benchmark.
module Geometry
export area, Circle, Rect
abstract type Shape end
struct Circle <: Shape
r::Float64
end
struct Rect <: Shape
w::Float64
h::Float64
end
area(s::Circle) = pi * s.r^2
area(s::Rect) = s.w * s.h
end
Now a second module, written later by someone else, adds a type:
module ExtraShapes
using ..Geometry: area, Shape
struct Triangle <: Shape
base::Float64
height::Float64
end
Geometry.area(t::Triangle) = 0.5 * t.base * t.height
end
Nothing in Geometry changed. area(Circle(1.0)) still evaluates to π for a unit circle, and area(Triangle(3.0, 4.0)) evaluates to 6.0. The qualified name Geometry.area(...) is required when defining the method from outside the module; writing a bare area(t::Triangle) after only using would create a separate function in ExtraShapes instead of extending the original. That's a common and quiet mistake.
Note also that the methods dispatch on concrete types (Circle, Rect, Triangle) rather than on the abstract Shape. That choice matters for the next section.
The bill: ambiguity, invalidation, and silent slowness
Open dispatch has costs, and they're mostly paid by the person adding methods.
- Ambiguity. If two methods are equally specific for a given call, Julia raises a
MethodErrorat the call, not at definition time. A classic shape isf(x::Integer, y::Number)alongsidef(x::Number, y::Integer):f(1, 1)matches both and neither wins. - Invalidation and compile latency. Adding a method can invalidate specializations the compiler already built, so new code may recompile dependent call sites. The first call into a path is typically slower than later calls; precompilation and workload-precompile tooling reduce this, but the details have shifted across Julia 1.x releases, so verify against your installed version.
- Type instability. If the compiler can't infer a concrete return type — because a struct field is untyped, a captured global isn't
const, or dispatch lands on an abstract type — the call goes through dynamic dispatch and may allocate. The code still produces correct answers. It's just slower, and nothing warns you.
The broader trade-off is cultural. Dispatch pushes you toward many small methods and informal interfaces (iteration, indexing, broadcasting) instead of an abstract base class with a required method set. That's flexible, but it moves the burden of "did I implement the right methods?" onto you, with no compiler-enforced contract.
Checking dispatch behavior on your own machine
These run in the Julia REPL, where InteractiveUtils is already loaded. In a script, add using InteractiveUtils first. No special permissions are needed.
- Confirm the version:
julia --version. Start a fresh session so precompilation state doesn't hide first-call behavior. - Ask which method a call hits:
@which area(Triangle(3.0, 4.0)). It prints the file and line of the selected method. If it points somewhere unexpected, your method wasn't attached to the generic function you thought. - List candidates:
methods(area)shows every method and its signature. - Audit ambiguity in a test suite:
using Test; Test.detect_ambiguities(Geometry). An empty result is the healthy case; anything listed is a call that could fail. - Inspect inference on a hot path:
@code_warntype area(x). Look forAny,Union, or boxed variables in the body. Treat this as a heuristic — output differs between compiler versions and it isn't a guarantee. - Separate compile time from run time: time the first call, then time a warmed-up call, and compare. Repeat after precompiling a representative workload. Measure on your own machine rather than trusting a number from a blog post.
What to take away
Multiple dispatch is worth choosing when you expect other people — or other modules of your own — to add types later without editing your code. The price is that correctness and speed become things you verify rather than things the compiler enforces. A practical habit: put Test.detect_ambiguities in your test suite, reach for @which whenever dispatch surprises you, and run @code_warntype on the paths you actually measure.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.