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
ConceptJuliaObject-oriented equivalent
Method selectionOn the types of all argumentsOn the receiver's class only
Inspect methodsmethods(f), @whichdir(), IDE lookup
Add behaviourDefine a method for your own typeSubclass and override
No matching methodMethodError listing candidatesAttributeError
Equal specificityAn ambiguity error you must resolveThe MRO silently picks one
Default behaviourA less specific method f(x)A base-class implementation
Extending a library functionimport Base: show, then add a methodNot 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 iterate and your type works with for, comprehensions, collect and sum.
  • Extend a function you do not own with import Base: show and then define a method — never qualify a definition as Base.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}) = false and 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.

Syntax and the type system Packages, environments and performance

Last refreshed 2026-09-18.