Multiple dispatch
Methods are selected on the types of all their arguments, which replaces type tags, if/else chains and class hierarchies.
One name, many methods
describe(x::Integer) = "an integer: $x"
describe(x::AbstractFloat) = "a float: $x"
describe(x::AbstractString) = "text of length $(length(x))"
describe(x) = "something else: $(typeof(x))"
describe(3) # "an integer: 3"
describe(3.0) # "a float: 3.0"
describe("abc") # "text of length 3"
describe(:sym) # "something else: Symbol"
methods(describe) # the whole method table, with signatures# in a script: using InteractiveUtils
@which describe(3.0) # points at the AbstractFloat method
# the same idea replacing an if/else on a type tag
abstract type Payload end
struct Json <: Payload end
struct Csv <: Payload end
render(::Json, data) = "{" * join(("\"$k\":$v" for (k, v) in pairs(data)), ",") * "}"
render(::Csv, data) = join(keys(data), ",") * "\n" * join(values(data), ",")
data = (a = 1, b = 2) # a NamedTuple keeps its field order
render(Json(), data) # {"a":1,"b":2}
render(Csv(), data) # a,b then a newline then 1,2| Concept | Julia | Object-oriented equivalent |
|---|---|---|
| Method selection | On the types of all arguments | On the receiver's class only |
| Inspect methods | methods(f), @which | dir(), IDE lookup |
| Add behaviour | Define a method for your own type | Subclass and override |
| No matching method | MethodError listing candidates | AttributeError |
| Equal specificity | An ambiguity error you must resolve | The MRO silently picks one |
| Default behaviour | A less specific method f(x) | A base-class implementation |
| Extending a library function | import Base: show, then add a method | Not possible without editing the class |
⚠️
Argument names do not take part in dispatch — only types do — so two methods that differ solely in parameter names are the same signature and the second silently replaces the first. Ambiguity is the other edge: when two methods are equally specific for a call, Julia raises a MethodError about the ambiguity rather than choosing one, and the fix is a method that resolves the intersection, not weaker annotations on both.
Interfaces are conventions
# any type with these methods works wherever the loop below is used
struct Countdown
from::Int
end
Base.length(c::Countdown) = c.from
Base.getindex(c::Countdown, i::Int) = c.from - i + 1
Base.iterate(c::Countdown, state = 1) =
state > c.from ? nothing : (c.from - state + 1, state + 1)
totals = 0
for n in Countdown(4)
totals += n
end
println(totals) # 10
sum(Countdown(4)) # 10, because sum only needs iterate- Duck typing is the norm: define
iterateand your type works withfor, comprehensions,collectandsum. - Extend a function you do not own with
import Base: showand then define a method — never qualify a definition asBase.show(x::T) = ..., which creates a separate function instead of a method. - The same pattern underlies broadcasting: define
Base.broadcastable(x)for a container-like type and the dot syntax works on it. - Traits let you branch on capability rather than type: define
is_sorted(::Type{T}) = falseand dispatch on the trait instead of listing types. - Because methods are just functions with signatures, a package can add support for your type without either side importing the other.
FAQ
What is the difference between import and using?
using brings exported names into scope; import brings only the module name, so you write Pkg.add or explicitly extend Base.show. To add a method to an existing function you always need import or a qualified call in the method definition.Can I dispatch on values, not types?
Yes, through the type system: define a type whose parameter is the value, such as
struct Fixed{N} end and Fixed{3}(). This is how dimensionality and units packages get compile-time checks with no runtime cost.Related
Syntax and the type system Packages, environments and performance
Last refreshed 2026-09-18.