Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Julia  Module 6 – The type system and multiple dispatch

Methods and multiple dispatch

Add methods to Julia functions and predict which one a call runs, resolve ambiguities, extend Base without type piracy, and write generic code that composes.

  • Intermediate
  • 15 minutes
  • Examples run with julia 1.13.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Add methods to a generic function and predict which one a call runs
  • Resolve a method ambiguity by defining the method for the overlap
  • Extend Base functions for your own types without type piracy
  • Write one generic algorithm that runs on Float64, BigFloat and Rational inputs

Before you start

What you’ll learn · Before you start

What you will learn

  • Add methods to a generic function and predict which one a call runs
  • Resolve a method ambiguity by defining the method for the overlap
  • Extend Base functions for your own types without type piracy
  • Write one generic algorithm that runs on Float64, BigFloat and Rational inputs

Before you start

On this page

+ alone has 155 methods in Julia 1.13.1, and 1 + 2.0, 1//2 + 1//3 and [1, 2] + [3, 4] each run a different one. Which one runs is decided at the moment of the call, from the types of all the arguments, and that single rule lets a solver written for floating-point numbers run unchanged on exact fractions.

This lesson adds methods to functions and predicts which one a call runs, resolves the ambiguities that two methods can create, extends Base for your own types without changing other people’s code, and writes a generic algorithm that runs on five kinds of number. The runs use Julia 1.13.1.

A function is a set of methods, and a call runs one of them

Heat leaves a wall in three ways: by conduction through a layer, by convection into the air and by radiation from the surface. Each has its own formula, so heat_flux gets one method: One definition of a function for some types of arguments. A Julia function can have many methods, and each call runs the most specific one that fits the argument types. More in “What Julia is for, and when to choose it” per kind of path, and a vector holds all three:

heat_transfer.jl: three methods of heat_flux Julia · heat_transfer.jl
# Three ways heat leaves a wall, and one method of heat_flux for each.
abstract type HeatPath end
struct Conduction <: HeatPath       # through a layer: k·A·ΔT / d
    k::Float64                      # conductivity, W/(m·K)
    area::Float64                   # m²
    thickness::Float64              # m
end
struct Convection <: HeatPath       # into moving air: h·A·ΔT
    h::Float64                      # W/(m²·K)
    area::Float64
end
struct Radiation <: HeatPath        # from the surface: ε·σ·A·(T₁⁴ − T₂⁴)
    emissivity::Float64
    area::Float64
end
const σ = 5.670374419e-8            # Stefan–Boltzmann constant, W/(m²·K⁴)

heat_flux(p::Conduction, hot, cold) = p.k * p.area * (hot - cold) / p.thickness
heat_flux(p::Convection, hot, cold) = p.h * p.area * (hot - cold)
heat_flux(p::Radiation, hot, cold) = p.emissivity * σ * p.area * (hot^4 - cold^4)

paths = HeatPath[Conduction(0.8, 2.0, 0.2), Convection(10.0, 2.0), Radiation(0.9, 2.0)]
for p in paths                      # each call picks the method for the type of p
    println(rpad(string(nameof(typeof(p))), 11), round(heat_flux(p, 350.0, 300.0); digits = 1), " W")
end
println("total:     ", round(sum(p -> heat_flux(p, 350.0, 300.0), paths); digits = 1), " W")
println(methods(heat_flux))
try
    heat_flux("brick", 350.0, 300.0)
catch err
    println(first(split(sprint(showerror, err), '\n')))
end
heat_transfer.jl, run with Julia 1.13.1 Python · heat_transfer_session.py
from session import files, run

# One function, three methods, called on a vector that holds all three kinds of path.
files("heat_transfer.jl")
run("""
julia heat_transfer.jl
""")

Output

$ julia heat_transfer.jl
Conduction 400.0 W
Convection 1000.0 W
Radiation  704.9 W
total:     2104.9 W
# 3 methods for generic function "heat_flux" from Main:
 [1] heat_flux(p::Radiation, hot, cold)
     @ ~/demo/heat_transfer.jl:20
 [2] heat_flux(p::Convection, hot, cold)
     @ ~/demo/heat_transfer.jl:19
 [3] heat_flux(p::Conduction, hot, cold)
     @ ~/demo/heat_transfer.jl:18
MethodError: no method matching heat_flux(::String, ::Float64, ::Float64)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 heat_transfer_session.py

heat_flux is one generic function: A Julia function seen as the set of all its methods, such as + with its 155 methods. A call runs one of them, chosen from the types of all the arguments. with 3 methods, and methods(heat_flux) listed them. The loop has no if on the type of p: each call ran the method for the path it was given, 400.0 W through a 0.2 m layer, 1,000.0 W into the air and 704.9 W by radiation from a surface at 350 K, with σ, the Stefan–Boltzmann constant, taken from its CODATA value. A call that no method accepts, heat_flux("brick", 350.0, 300.0), stopped with a MethodError that names the argument types it was given. Step through how one call finds its method:

Interactive · stage by stage How a call finds its method Step down the column for one call of heat_flux, then follow the two branches a call can take when it fails.
How a call finds its method, step 7 of 7. Showing the call, every argument's type, the methods that fit, the most specific one, its compiled code runs, MethodError: no match, MethodError: ambiguous. In focus: MethodError: ambiguous.none fitsno single winnerthe callheat_flux(p, 350.0, 300.0)every argument's typeConvection, Float64, Float64the methods that fitheat_flux(p::Convection, …)the most specific oneone method fits hereits compiled code runs1000.0 WMethodError: no matchheat_flux("brick", …)MethodError: ambiguouscombine(1, 2)no single winner

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The call. The loop calls heat_flux(p, 350.0, 300.0) with p taken from a Vector{HeatPath}. The vector's element type says only that p is some HeatPath.
  2. Every argument counts. At this call p is a Convection, and the temperatures are Float64. Julia looks at the types of all three arguments, not only the first.
  3. Which methods fit. Of the 3 methods of heat_flux, only heat_flux(p::Convection, hot, cold) accepts a Convection as its first argument.
  4. The most specific. When several methods fit, the most specific one is chosen. Here one fits, so it is the one.
  5. It runs. Its code, compiled for these argument types, computes 10.0 × 2.0 × 50.0 = 1000.0 W, as the run printed for Convection.
  6. No method fits. heat_flux("brick", 350.0, 300.0) has no method for a String, so the call stopped with MethodError: no method matching heat_flux(::String, ::Float64, ::Float64).
  7. Two fit equally. In ambiguity.jl both combine methods fit combine(1, 2) and neither is more specific, so Julia refused to guess: MethodError: combine(::Int64, ::Int64) is ambiguous.

A new kind of path needs a new type and a method, and nothing that already exists changes: the loop, sum and every other caller work with it at once.

Dispatch looks at the types of every argument

The methods of heat_flux differ only in their first argument. The manual calls choosing a method from all of a function’s arguments multiple dispatch: Choosing which method of a function to run from the number and the types of all its arguments, not only the first, as Julia does on every call. More in “What Julia is for, and when to choose it”, and distance between points and circles needs it, because the formula depends on both shapes:

distance.jl: a method for each pair of shapes, and five calls of + Julia · distance.jl
# distance(a, b) between points and circles: one method for each pair of argument types.
abstract type Shape end
struct Point <: Shape
    x::Float64
    y::Float64
end
struct Circle <: Shape
    center::Point
    r::Float64
end
distance(p::Point, q::Point) = hypot(p.x - q.x, p.y - q.y)
distance(p::Point, c::Circle) = max(0.0, distance(p, c.center) - c.r)
distance(c::Circle, p::Point) = distance(p, c)
distance(a::Circle, b::Circle) = max(0.0, distance(a.center, b.center) - a.r - b.r)

o = Point(0, 0)
c = Circle(Point(3, 4), 2)
println("point to point:   ", distance(o, Point(3, 4)))
println("point to circle:  ", distance(o, c))
println("circle to point:  ", distance(c, o))
println("circle to circle: ", distance(Circle(o, 1), c))

# Base's + works the same way: which method runs depends on both arguments.
for (code, types) in (("1.5 + 2.5", (Float64, Float64)), ("1 + 2.0", (Int, Float64)), ("1//2 + 1//3", (Rational{Int}, Rational{Int})),
                      ("1 + 2im", (Int, Complex{Int})), ("[1, 2] + [3, 4]", (Vector{Int}, Vector{Int})))
    m = which(+, types)
    println(rpad(code, 17), first(split(sprint(show, m), " @")), "   (", m.file, ")")
end
println("+ has ", length(methods(+)), " methods")
distance.jl, run with Julia 1.13.1 Python · distance_session.py
from session import files, run

# Four methods of distance, and the methods of + that five calls pick.
files("distance.jl")
run("""
julia distance.jl
""")

Output

$ julia distance.jl
point to point:   5.0
point to circle:  3.0
circle to point:  3.0
circle to circle: 2.0
1.5 + 2.5        +(x::T, y::T) where T<:Union{Float16, Float32, Float64}   (float.jl)
1 + 2.0          +(x::Number, y::Number)   (promotion.jl)
1//2 + 1//3      +(x::Rational, y::Rational)   (rational.jl)
1 + 2im          +(x::Real, z::Complex)   (complex.jl)
[1, 2] + [3, 4]  +(A::Array, Bs::Array...)   (arraymath.jl)
+ has 155 methods

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 distance_session.py

Each pair of types has its own method, and the call picks by both: 5.0 from the origin to the point (3, 4), 3.0 to the circle of radius 2 around it, in either order, and 2.0 between that circle and a circle of radius 1 at the origin. In a language that dispatches only on the first argument, the second shape would need a test inside each method.

which shows the method a call would run, and + works the same way. Two Float64 values met a method for floating-point types, an Int64 and a Float64 met the catch-all +(x::Number, y::Number) of promotion.jl, which converts both and tries again, and two vectors met the method for arrays.

Branching on the type inside one method

Wrong: distance(a, b) = if a isa Point && b isa Circle … elseif …, one long method that every new shape must edit. Right: one short method per pair of types, so a new shape adds methods and leaves the existing ones as they are.

Checkpoint

Check: A function as a set of methods · Dispatch on every argument

  1. Question 1 of 2 You add a fourth kind of heat path, Evaporation <: HeatPath. What else must change for the loop over paths to handle it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only a new method heat_flux(p::Evaporation, hot, cold)

    With one method per type, new types extend a function without changing the code that calls it.

  2. Question 2 of 2 Which method of + does 1 + 2.0 run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: +(x::Number, y::Number), which promotes both arguments and calls + again

    Dispatch uses both argument types; for an Int64 and a Float64 the most specific fit is the generic Number method, which converts both to a common type.

The most specific method wins

Methods may overlap: a method for Real also fits every Integer. The manual’s rule is that when several methods fit, the most specific one applicable to the arguments runs. Three methods of kind, for three levels of the tree:

specificity.jl: three overlapping methods, six values Julia · specificity.jl
# When several methods fit a call, the most specific one runs.
kind(x::Number) = "a number"
kind(x::Real) = "a real number"
kind(x::Integer) = "an integer"
for x in (7, 2.5, 1 + 2im, true, big(10)^30, 0x0f)
    println(rpad(repr(x), 34), kind(x))
end

# invoke runs a less specific method on purpose.
println("invoke(kind, Tuple{Real}, 7):   ", invoke(kind, Tuple{Real}, 7))
println("invoke(kind, Tuple{Number}, 7): ", invoke(kind, Tuple{Number}, 7))

# Julia never converts an argument to make a method fit.
area(w::Float64, h::Float64) = w * h
try
    area(2, 3.5)
catch err
    println(first(split(sprint(showerror, err), '\n')))
end
specificity.jl, run with Julia 1.13.1 Python · specificity_session.py
from session import files, run

# Three methods of kind, six values, and a method that does not fit at all.
files("specificity.jl")
run("""
julia specificity.jl
""")

Output

$ julia specificity.jl
7                                 an integer
2.5                               a real number
1 + 2im                           a number
true                              an integer
1000000000000000000000000000000   an integer
0x0f                              an integer
invoke(kind, Tuple{Real}, 7):   a real number
invoke(kind, Tuple{Number}, 7): a number
MethodError: no method matching area(::Int64, ::Float64)

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 specificity_session.py

7, true, big(10)^30 and 0x0f are all integers, so each ran the Integer method; 2.5 ran the Real method and 1 + 2im the Number method. “More specific” follows the tree: Integer <: Real <: Number, so the method for the narrower type wins whenever it fits. invoke runs a less specific method on purpose, which a method uses to call the general version it extends, as invoke(kind, Tuple{Real}, 7) did.

The last call shows what dispatch does not do. area(w::Float64, h::Float64) did not accept area(2, 3.5): Julia never converts an argument to make a method fit, as the manual stresses, so the Int64 2 found no method. Either accept Real and let the arithmetic convert, or convert at the call.

Key idea

A call runs the most specific method that fits the types of all its arguments, and an argument is never converted to make a method fit.

When two methods fit equally well, Julia refuses to guess

Two methods can both fit a call without either being more specific. Here one accepts an integer first and anything real second, the other the reverse, and combine(1, 2) fits both:

ambiguity.jl: an ambiguity and the method that resolves it Julia · ambiguity.jl
# Two methods that both fit a call, with neither more specific: an ambiguity.
module Mixing
combine(a::Integer, b::Real) = "integer first"
combine(a::Real, b::Integer) = "integer second"
end
using .Mixing: combine
println(combine(1, 2.5), " | ", combine(2.5, 1))
try
    combine(1, 2)
catch err
    println(sprint(showerror, err))
end

using Test
println("ambiguities found: ", length(Test.detect_ambiguities(Mixing)))
Mixing.eval(:(combine(a::Integer, b::Integer) = "both integers"))   # the method for the overlap
println(combine(1, 2))
println("ambiguities found: ", length(Test.detect_ambiguities(Mixing)))
ambiguity.jl, run with Julia 1.13.1 Python · ambiguity_session.py
from session import files, run

# An ambiguity, what Julia says about it, and the method that resolves it.
files("ambiguity.jl")
run("""
julia ambiguity.jl
""")

Output

$ julia ambiguity.jl
integer first | integer second
MethodError: combine(::Int64, ::Int64) is ambiguous.

Candidates:
  combine(a::Real, b::Integer)
    @ Main.Mixing ~/demo/ambiguity.jl:4
  combine(a::Integer, b::Real)
    @ Main.Mixing ~/demo/ambiguity.jl:3

Possible fix, define
  combine(::Integer, ::Integer)

ambiguities found: 1
both integers
ambiguities found: 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ambiguity_session.py

The manual says that in such a method ambiguity: A call that two or more methods fit equally well, with none more specific than the others. Julia throws a MethodError instead of choosing; defining the method for the overlap resolves it. Julia raises a MethodError rather than arbitrarily picking a method, and the error lists both candidates and suggests the fix: define combine(::Integer, ::Integer), the method for the overlap. Once it existed, combine(1, 2) returned “both integers”. The manual recommends defining the method for the overlap first, before the two methods that create it.

Ambiguities hide until a call happens to land in the overlap, perhaps months later in someone else’s code. Test.detect_ambiguities(Mixing) finds them without making any such call: it reported 1 for the module, and 0 after the fix, so a test can assert that a package has none; Aqua.jl, a package of quality checks, runs the same test with others.

Avoid ambiguities by design

Dispatch on one argument at a time where you can, or convert the arguments to one type first and keep a single method for that type: the manual names both. Then put @test isempty(Test.detect_ambiguities(MyPackage)) in your tests.

Checkpoint

Check: The most specific method wins · Method ambiguities and their fix

  1. Question 1 of 2 With methods kind(x::Number), kind(x::Real) and kind(x::Integer), what does kind(0x0f) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: "an integer"

    All three methods fit a UInt8; the one for the narrowest type, Integer, wins.

  2. Question 2 of 2 combine(a::Integer, b::Real) and combine(a::Real, b::Integer) make combine(1, 2) ambiguous. What is the fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Define combine(a::Integer, b::Integer), the method for the overlap

    Define the method for the intersection of the two signatures, ideally before the two that create it, and test with Test.detect_ambiguities.

Extend Base for your own types, never for Base’s types

Adding a method to a Base function for your own type is how a type joins the rest of Julia. Interval below gets a + and a show, and sum works on a vector of intervals without being told anything about them:

extend_base.jl: + for intervals, then a pirated + for strings Julia · extend_base.jl
# Adding methods to Base functions for your own type, and what type piracy does to other code.
struct Interval
    lo::Float64
    hi::Float64
end
Base.:+(a::Interval, b::Interval) = Interval(a.lo + b.lo, a.hi + b.hi)
Base.show(io::IO, x::Interval) = print(io, "[", x.lo, ", ", x.hi, "]")

legs = [Interval(2.0, 2.5), Interval(1.0, 1.25), Interval(3.0, 3.5)]   # travel times in hours
println(legs[1] + legs[2])
println("sum(legs) = ", sum(legs))           # sum is generic: it needs only + for Interval

# Code that has never heard of the module below.
module Report
function label(parts)
    try
        return parts[1] + parts[2]
    catch err
        err isa MethodError || rethrow()
        return "MethodError, as expected"
    end
end
end
println("before: ", Report.label(["km", "/h"]))

module Shortcuts
# Type piracy: a Base function, for Base types, defined in a module that owns neither.
Base.:+(a::String, b::String) = a * b
end
println("after:  ", Report.label(["km", "/h"]))
extend_base.jl, run with Julia 1.13.1 Python · extend_base_session.py
from session import files, run

# A + method for an Interval type, then a pirated + for strings and what it does to unrelated code.
files("extend_base.jl")
run("""
julia extend_base.jl
""")

Output

$ julia extend_base.jl
[3.0, 3.75]
sum(legs) = [6.0, 7.25]
before: MethodError, as expected
after:  km/h

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extend_base_session.py

Three legs of a journey, taking 2 to 2.5, 1 to 1.25 and 3 to 3.5 hours, added up to [6.0, 7.25]: sum is generic code that only needs + for its elements. The second half of the run is the opposite case. The module Shortcuts defined + for two Strings, and owns neither the function nor the type. The style guide calls this type piracy: Adding a method to a function you do not own, such as Base's +, for argument types you do not own either. Every module shares the function, so the method changes code that never asked for it. and warns that it can introduce incompatibilities that are hard to predict. Step through what it did to a module that never mentions Shortcuts:

Interactive · message by message A pirated method changes code that never asked for it Step through two calls from the Report module, before and after another module adds a method to Base's +.
A pirated method changes code that never asked for it, step 5 of 5. Messages so far: module Report to Base.:+: "km" + "/h"; Base.:+ to module Report: MethodError; module Shortcuts to Base.:+: +(::String, ::String); module Report to Base.:+: "km" + "/h"; Base.:+ to module Report: "km/h".module Reportadds "km" and "/h"Base.:+one function for all codemodule Shortcutsowns neither + nor String"km" + "/h"MethodError+(::String, ::String)"km" + "/h""km/h"

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The first call. Report.label computes "km" + "/h". + has no method for two Strings, so the call fails.
  2. A MethodError. Report catches the MethodError and prints "MethodError, as expected": joining strings is written with *, not +.
  3. The pirate. Shortcuts defines Base.:+(a::String, b::String). It owns neither the function + nor the type String: that is type piracy.
  4. The same call again. Report has not changed and never mentions Shortcuts, but the method table of + has: the same call now finds the pirated method.
  5. A different answer. The call returned "km/h" instead of an error. Every module that uses + sees the new method, which is why the style guide asks you to avoid type piracy.

Report.label was unchanged, yet it returned “km/h” instead of its MethodError, because methods belong to the function and every module shares Base.:+. Aqua.jl also checks a package for obvious piracy. Who owns the function and who owns the types decides each case:

You define For Verdict
Base.:+(a::Interval, b::Interval) your type Extending Base: the way a type joins in
Base.show(io::IO, x::Interval) your type Extending Base
Base.:+(a::String, b::String) Base’s types Type piracy: changes everyone’s +
mytotal(v::Vector{Float64}) Base’s types, your function Fine: the function is yours, so no other code changes

Checkpoint

Check: Extending Base without type piracy

  1. Question 1 of 2 Your package defines the type Interval. Which of these definitions are type piracy? Choose every one that is.

    Choose every answer that is right.

    Show the answer: Question 1 of 2

    Answer:

    • Base.length(s::Symbol) = length(string(s))
    • Base.:+(a::String, b::String) = a * b

    A method is piracy when you own neither the function nor any of the types in its signature.

  2. Question 2 of 2 Why did sum(legs) work on a vector of Intervals?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: sum is generic: it only needs + for the elements, and Interval has a + method

    Generic functions are written against operations, not types, so a type that defines the operations gets the functions for nothing.

One generic algorithm runs on number types it has never seen

Newton’s method improves a guess x for a root of f with x - f(x) / df(x). Written without a single type annotation, the same function runs on whatever number it is given, and the arithmetic dispatches to that type’s methods:

generic_newton.jl: five Newton steps on five kinds of number Julia · generic_newton.jl
# One Newton solver for f(x) = 0, written without a single type, run on five kinds of number.
function newton(f, df, x; steps = 5)
    for _ in 1:steps
        x = x - f(x) / df(x)
    end
    return x
end
f(x) = x^2 - 2
df(x) = 2x
for x0 in (1.0f0, 1.0, big(1.0), 1//1, big(1)//1)
    r = newton(f, df, x0)
    println(rpad(string(typeof(r)), 17), "error ", Float64(abs(r - sqrt(big(2)))))
end
println("5 steps from 1//1: ", newton(f, df, 1//1))
try
    newton(f, df, 1//1; steps = 6)
catch err
    println(sprint(showerror, err))
end
println("6 steps from big(1)//1: ", newton(f, df, big(1)//1; steps = 6))
generic_newton.jl, run with Julia 1.13.1 Python · generic_newton_session.py
from session import files, run

# The same five Newton steps on Float32, Float64, BigFloat and two kinds of Rational.
files("generic_newton.jl")
run("""
julia generic_newton.jl
""")

Output

$ julia generic_newton.jl
Float32          error 2.420323420895794e-8
Float64          error 9.667293313452913e-17
BigFloat         error 8.992928321650453e-25
Rational{Int64}  error 8.992928321650453e-25
Rational{BigInt} error 8.992928321650453e-25
5 steps from 1//1: 886731088897//627013566048
OverflowError: 886731088897 * 886731088897 overflowed for type Int64
6 steps from big(1)//1: 1572584048032918633353217//1111984844349868137938112

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 generic_newton_session.py

Each run came back in the type it started with, and each type set its own limit. Float32 stopped at an error of 2.4e-8 and Float64 at 9.7e-17, each as close to √2 as its type can get. BigFloat reached 9.0e-25 after the same five steps and would keep improving. The Rational runs stayed exact, the fraction 886731088897//627013566048, but a sixth step squared its numerator and stopped with OverflowError for Int64: Rational{Int64} checks its arithmetic for overflow. With BigInt inside, Rational{BigInt} took the sixth step.

Nobody wrote newton for fractions. It works because -, /, ^ and * have methods for every one of these types, and a type written later, such as the dual numbers of the lesson on custom number types, joins in by defining them.

Values as types multiply compiled code

Val(42) turns a value into a type, so a method can dispatch on it, as f(::Val{N}) where {N} does. The performance tips warn against overusing this: every distinct value becomes a type of its own, every function that receives it is compiled again for it, and too many such types waste compile time and memory.

Checkpoint

Check: Generic algorithms over number types

  1. Question 1 of 2 What type does newton(f, df, 1.0f0) return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Float32

    Generic code takes its types from its inputs, so each run came back in the type it started with.

  2. Question 2 of 2 Why did the sixth Newton step from 1//1 stop with an OverflowError, while big(1)//1 went on?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Rational{Int64} keeps exact fractions in 64-bit integers and checks them for overflow; BigInt grows instead

    A generic algorithm inherits each type's limits: Float32 its precision, Rational{Int64} the range of Int64.

Practise: particles that interact

Symmetric methods without ambiguities

Define abstract type Particle end and the types Electron, Proton and Neutron, and a method interaction(a::Particle, b::Particle) that returns “repel” or “attract” from the signs of their charges. Add interaction(a::Neutron, b::Particle) returning “none”, then the method that makes interaction(Proton(), Neutron()) give the same answer. Check on your machine that Test.detect_ambiguities(Main) names the overlap you have just created, and that it finds nothing once you add the method for two neutrons.

The self-check below asks about methods, dispatch on every argument, specificity, ambiguities, piracy and generic code, starting where you are least sure; each answer is explained the moment you give it:

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 How many methods did methods(heat_flux) list?

    Type a number.

    Show the answer: Question 1 of 12

    Answer: 3 methods

    One method per kind of heat path: Conduction, Convection and Radiation.

  2. Question 2 of 12 What did heat_flux("brick", 350.0, 300.0) do?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: It threw MethodError: no method matching heat_flux(::String, ::Float64, ::Float64)

    Dispatch either finds a method that fits all the argument types or throws a MethodError naming them.

  3. Question 3 of 12 Which method of distance does distance(Circle(o, 1), c) run, with c a Circle?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: distance(a::Circle, b::Circle)

    Multiple dispatch picks the method from every argument's type, so each pair of shapes has its own method.

  4. Question 4 of 12 In a language that dispatches only on the first argument, how would distance(a, b) for every pair of shapes usually be written?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: A method on the first shape that tests the type of the second one

    Multiple dispatch removes the type tests that single dispatch pushes into the methods.

  5. Question 5 of 12 Only area(w::Float64, h::Float64) is defined. What does area(2, 3.5) do?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: It throws a MethodError: 2 is an Int64, and Julia never converts it to fit

    Accept Real and let the arithmetic convert, or convert at the call; dispatch matches types as they are.

  6. Question 6 of 12 What does invoke(kind, Tuple{Real}, 7) return?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: "a real number"

    invoke lets a specific method call the general one it extends, without dispatching back to itself.

  7. Question 7 of 12 What does Julia do when two methods fit a call equally well and neither is more specific?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: It throws a MethodError saying the call is ambiguous, and lists the candidates

    An ambiguity is an error at the call, so it shows up only when a call lands in the overlap.

  8. Question 8 of 12 Why put @test isempty(Test.detect_ambiguities(MyPackage)) in a package's tests?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: It finds ambiguities before any user call lands in the overlap

    Ambiguities hide until a particular combination of types is called, so a test that searches for them is cheaper than a bug report.

  9. Question 9 of 12 What is type piracy?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: Defining a method of a function you do not own for argument types you do not own

    Methods belong to the function, which every module shares, so a pirated method changes code that never asked for it, as Report.label showed.

  10. Question 10 of 12 After Shortcuts defined + for two Strings, what changed for Report, a module that never mentions it?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Its "km" + "/h" returned "km/h" instead of throwing a MethodError

    A pirated method changes behaviour everywhere, which is why such bugs are hard to trace.

  11. Question 11 of 12 After five steps from 1, which run ended closest to √2?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: BigFloat and the two Rational runs, all with an error of 9.0e-25

    The algorithm was the same; each number type decided how close it could get.

  12. Question 12 of 12 You write a root finder and want it to run on any number type a user brings, including ones written later. What should its signature look like?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: No type annotations on f, df or x, and arithmetic only through generic operations

    Generic code names operations, not types, which is what lets types the author never saw compose with it.

Interview questions

Warm-up (fresher to 2–5 years): how is multiple dispatch different from method overloading in Java or C++?

Overloading in Java or C++ chooses among functions of the same name at compile time, from the declared types of the arguments, and virtual methods add a run-time choice on one object only, the receiver. Julia chooses the method on every call, from the run-time types of all the arguments, and picks the most specific one that fits. So distance(c, p) finds the method for a circle and a point even when both come out of a Vector{Shape}, where overloading would see only the declared type Shape. The methods belong to the function, not to a class, so anyone can add a method for their own types to an existing function, and generic code such as sum works with it at once. Julia raises an error instead of guessing when two methods fit equally well.

Two design questions grow out of this lesson: two packages whose methods collide, and a shortcut that changes how everyone’s code behaves. For each, name the methods involved before you look at the first step:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to staff

    Two packages add methods to a shared function combine(a, b): one for (MyMatrix, AbstractMatrix), the other for (AbstractMatrix, TheirMatrix). Users who load both get "ambiguous" MethodErrors for some calls. How do you find every ambiguity, fix the one you have, and keep new ones from appearing?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to senior

    A colleague adds Base.:+(v::Vector{Float64}, x::Float64) = v .+ x to a project's utilities so that v + 1.0 works. What is wrong with it, what can it break, and what would you do instead?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A generic function is a set of methods, and each call runs one of them, chosen from the types of all its arguments when the call happens.
  • A vector of mixed types needs no type tests: heat_flux ran the method for each path, and a new type adds methods without changing existing code.
  • The most specific method that fits wins; Julia never converts an argument to make a method fit, and invoke calls a less specific method on purpose.
  • Two methods that fit a call equally well are an ambiguity, a MethodError; define the method for the overlap and test with Test.detect_ambiguities.
  • Extend Base functions for your own types; defining them for types you do not own, type piracy, changed what an unrelated module’s + returned.
  • Generic code takes its arithmetic from its arguments: one newton ran on five kinds of number, each with its own limit, Rational{Int64} with an OverflowError on its sixth step.

Terms in this lesson

generic function
A Julia function seen as the set of all its methods, such as + with its 155 methods. A call runs one of them, chosen from the types of all the arguments.
method ambiguity
A call that two or more methods fit equally well, with none more specific than the others. Julia throws a MethodError instead of choosing; defining the method for the overlap resolves it.
type piracy
Adding a method to a function you do not own, such as Base's +, for argument types you do not own either. Every module shares the function, so the method changes code that never asked for it.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.