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

Parametric types and generic numeric code

Define parametric Julia structs with promoting constructors, tie argument types with where clauses, keep data in its own number type, and drop abstract fields.

  • 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

  • Define parametric structs, with an outer constructor that promotes mixed inputs
  • Write methods with where clauses that tie argument types together
  • Write numeric code that keeps Float32, BigFloat and Rational inputs in their own type
  • Replace abstract field types with type parameters, and measure the allocations saved

Before you start

What you’ll learn · Before you start

What you will learn

  • Define parametric structs, with an outer constructor that promotes mixed inputs
  • Write methods with where clauses that tie argument types together
  • Write numeric code that keeps Float32, BigFloat and Rational inputs in their own type
  • Replace abstract field types with type parameters, and measure the allocations saved

Before you start

On this page

A struct whose fields are declared AbstractVector and Real made one pass over a million numbers allocate 95,991,840 bytes. The same struct with type parameters allocated 0, for the same data and the same loop. And a mean that starts its sum at 0.0 turned Float32 data into Float64 and exact fractions into rounded ones.

This lesson defines structs with type parameters and constructors that promote mixed inputs, writes methods with where clauses, keeps numeric code in the type of its data, removes abstract field types, and puts a length into a type with NTuple. The runs use Julia 1.13.1.

A type parameter turns one definition into a family of types

struct Point{T<:Real} declares a type parameter: A type written in braces after a type's name, such as T in Point{T}, that the type's fields or a method's arguments refer to. Each value of T gives another type of the family, such as Point{Float64}. T, and both fields have that type. The definition makes one parametric type: A type defined with type parameters, such as struct Point{T<:Real}. It stands for a whole family of types, one concrete member for each parameter, and alone it is a UnionAll type., Point, and a concrete type for every real T:

point.jl: Point{T}, two calls it refuses, and a promoting constructor Julia · point.jl
# One definition, a family of types: Point{T} for every real type T.
struct Point{T<:Real}
    x::T
    y::T
end
println(Point(1, 2), "   ", Point(1.5, 2.0), "   ", Point(1//2, 3//4))
for args in ((1, 2.5), ("a", "b"))
    try
        Point(args...)
    catch err
        println(first(split(sprint(showerror, err), '\n')))
    end
end

# An outer constructor that promotes mixed arguments to one type first.
Point(x::Real, y::Real) = Point(promote(x, y)...)
println(Point(1, 2.5), "   ", Point(1, 2//3), "   ", Point(1, big(2.0)))

println("Point is a ", typeof(Point), "; Point{Float64} <: Point: ", Point{Float64} <: Point,
        "; Point{Float64} <: Point{Real}: ", Point{Float64} <: Point{Real})
println("sizeof: Point{Float64} ", sizeof(Point{Float64}), " bytes, Point{Int32} ", sizeof(Point{Int32}),
        " bytes; isbitstype(Point{Float64}): ", isbitstype(Point{Float64}), ", isbitstype(Point{BigFloat}): ", isbitstype(Point{BigFloat}))
point.jl, run with Julia 1.13.1 Python · point_session.py
from session import files, run

# A parametric Point, the calls its default constructor refuses, and a promoting constructor.
files("point.jl")
run("""
julia point.jl
""")

Output

$ julia point.jl
Point{Int64}(1, 2)   Point{Float64}(1.5, 2.0)   Point{Rational{Int64}}(1//2, 3//4)
MethodError: no method matching Point(::Int64, ::Float64)
MethodError: no method matching Point(::String, ::String)
Point{Float64}(1.0, 2.5)   Point{Rational{Int64}}(1//1, 2//3)   Point{BigFloat}(1.0, 2.0)
Point is a UnionAll; Point{Float64} <: Point: true; Point{Float64} <: Point{Real}: false
sizeof: Point{Float64} 16 bytes, Point{Int32} 8 bytes; isbitstype(Point{Float64}): true, isbitstype(Point{BigFloat}): false

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

Point(1, 2) built a Point{Int64}, Point(1.5, 2.0) a Point{Float64} and Point(1//2, 3//4) a Point{Rational{Int64}}: Julia read T from the arguments. Point("a", "b") failed because String is not a subtype of Real, the bound the struct declares. Point(1, 2.5) failed for another reason, which the manual’s page on constructors describes: the default constructor needs both arguments to have one type T. Step through the call:

Interactive · stage by stage Point(1, 2.5): no T fits, until promote makes one Step down the column for the call with the outer constructor, then see the branch the call took before it existed.
Point(1, 2.5): no T fits, until promote makes one, step 6 of 6. Showing Point(1, 2.5), the default constructor, the outer constructor, promote(1, 2.5), Point{Float64}(1.0, 2.5), MethodError. In focus: Point{Float64}(1.0, 2.5).no single TPoint(1, 2.5)an Int64 and a Float64the default constructorPoint(x::T, y::T): one Tthe outer constructorPoint(x::Real, y::Real)promote(1, 2.5)(1.0, 2.5): both Float64Point{Float64}(1.0, 2.5)T = Float64MethodErrorbefore the outer constructor

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

Every step as text
  1. The call. Point(1, 2.5) passes an Int64 and a Float64. The struct declares x::T and y::T, one T for both fields.
  2. The default constructor. Julia's default constructor Point(x::T, y::T) needs both arguments of one type T. No T is both Int64 and Float64, so it does not fit.
  3. Without help, an error. Before the outer constructor was defined, nothing else fitted, and the call stopped with MethodError: no method matching Point(::Int64, ::Float64).
  4. The outer constructor. Point(x::Real, y::Real) = Point(promote(x, y)...) accepts any two reals, so the call now reaches it.
  5. promote. promote(1, 2.5) converts both to their common type and returns (1.0, 2.5), two Float64 values.
  6. One T. Now the default constructor fits with T = Float64, and the run printed Point{Float64}(1.0, 2.5). Point(1, 2//3) gave Point{Rational{Int64}} the same way.

The outer constructor Point(x::Real, y::Real) = Point(promote(x, y)...) is the manual’s own fix. promote converts its arguments to a common type, so Point(1, 2.5) became Point{Float64}(1.0, 2.5) and Point(1, 2//3) a Point{Rational{Int64}}.

Point on its own is a UnionAll type: A type with a parameter left open, written with where or <:, such as Vector{T} where T<:Real, also written Vector{<:Real}. It is the set of every type it covers and is never a value's own type., the set of all its members: Point{Float64} <: Point is true, while Point{Float64} <: Point{Real} is false, because parameters are invariant. Each member is stored as its parameter dictates: Point{Float64} takes 16 bytes and Point{Int32} 8, both plain data, while a Point{BigFloat} holds references to its numbers.

A parameter for a field that is always the same type

Wrong: struct Station{S<:AbstractString} name::S end when every name is a String, which only adds a parameter to every signature. Right: name::String. A parameter is worth it when a field’s type really varies, as the coordinates of a point or the vector inside a series do.

Checkpoint

Check: Parametric structs and promoting constructors

  1. Question 1 of 2 With only struct Point{T<:Real}; x::T; y::T; end defined, what does Point(1, 2.5) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws a MethodError: no single T is both Int64 and Float64

    Add Point(x::Real, y::Real) = Point(promote(x, y)...), so mixed arguments are converted to one type before the default constructor runs.

  2. Question 2 of 2 Which of these is true?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Point{Float64} <: Point

    A parametric type is a family: write Point in a signature to accept every member, and Point{Float64} for one.

A where clause names a parameter and ties arguments together

A method can have type parameters too, declared after the signature with where. A name used twice must stand for the same type in both places, which ties the arguments together; the manual’s page on methods calls such methods parametric:

where_clauses.jl: three methods with where clauses Julia · where_clauses.jl
# where names a type parameter of a method, and the same name ties arguments together.
same(x::T, y::T) where {T} = "both $(T)"
same(x, y) = "different types"
for (a, b) in ((1, 2), (1, 2.0), (1.0, 2.0), ("a", "b"), (1, true))
    println(rpad(repr(a) * ", " * repr(b), 12), same(a, b))
end

# T is available inside the method, here to return a zero of the element type.
lastvalue(v::AbstractVector{T}) where {T<:Real} = isempty(v) ? zero(T) : v[end]
println(lastvalue([1, 2, 3]), " ", lastvalue(Float64[]), " ", lastvalue(Int8[]), " ", typeof(lastvalue(Int8[])))
try
    lastvalue(["a"])
catch err
    println(first(split(sprint(showerror, err), '\n')))
end

# Two vectors with the same element type, and nothing else.
total_length(x::AbstractVector{T}, y::AbstractVector{T}) where {T} = length(x) + length(y)
println(total_length([1, 2], [3, 4]))
try
    total_length([1, 2], [3.0])
catch err
    println(first(split(sprint(showerror, err), '\n')))
end
where_clauses.jl, run with Julia 1.13.1 Python · where_clauses_session.py
from session import files, run

# Three methods with where clauses, and the calls each one accepts or refuses.
files("where_clauses.jl")
run("""
julia where_clauses.jl
""")

Output

$ julia where_clauses.jl
1, 2        both Int64
1, 2.0      different types
1.0, 2.0    both Float64
"a", "b"    both String
1, true     different types
3 0.0 0 Int8
MethodError: no method matching lastvalue(::Vector{String})
4
MethodError: no method matching total_length(::Vector{Int64}, ::Vector{Float64})

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

same(x::T, y::T) where {T} matched (1, 2) and (1.0, 2.0), but not (1, 2.0) or (1, true): T must be one concrete type, and Int64, Float64 and Bool are three. The catch-all same(x, y) took the rest.

T is also a name the body can use. lastvalue(v::AbstractVector{T}) where {T<:Real} returned zero(T) for an empty vector, so an empty Int8[] gave an Int8 zero, and <:Real refused a vector of strings. total_length demanded the same element type for both vectors and refused a Vector{Int64} with a Vector{Float64}. The three forms side by side:

Signature Accepts T inside the body
f(v::AbstractVector{<:Real}) any vector of reals no name for it; use eltype(v)
f(v::AbstractVector{T}) where {T<:Real} the same vectors T, the element type
f(x::AbstractVector{T}, y::AbstractVector{T}) where {T} two vectors of one element type T

Tie types only when the method needs it

A where clause that forces two arguments to share a type refuses Int64 with Float64, which is usually a normal call. Tie the types when the code really needs one type, for example to store both in one vector without conversion, and otherwise accept each argument on its own and promote inside.

Checkpoint

Check: Where clauses that tie argument types

  1. Question 1 of 2 With same(x::T, y::T) where {T} and the catch-all same(x, y), which call reaches the first method?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: same(1.0, 2.0)

    A type parameter used twice ties the two arguments to one concrete type.

  2. Question 2 of 2 What does lastvalue(Int8[]) return, with lastvalue(v::AbstractVector{T}) where {T<:Real} = isempty(v) ? zero(T) : v[end]?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An Int8 zero

    A where clause gives the body a name for the type, so it can build values of exactly that type.

Generic numeric code takes its types from the data

Two means of the same five inputs differ in one line. mean_fixed starts its sum at the literal 0.0, and mean_generic at zero(eltype(v)), a zero of the data’s own type:

generic_mean.jl: the type each mean returns Julia · generic_mean.jl
# Two means: one starts from the literal 0.0, the other from a zero of the data's own type.
function mean_fixed(v)
    s = 0.0
    for x in v
        s += x
    end
    return s / length(v)
end
function mean_generic(v)
    s = zero(eltype(v))
    for x in v
        s += x
    end
    return s / length(v)
end
println(rpad("input", 16), rpad("mean_fixed", 11), rpad("mean_generic", 17), "mean_generic's value")
for v in (Float32[1.5, 2.5, 4.0], [1.5, 2.5, 4.0], big.([1.5, 2.5, 4.0]), [1//3, 1//6, 1//2], [1, 2, 4])
    println(rpad(string(eltype(v)), 16), rpad(string(typeof(mean_fixed(v))), 11),
            rpad(string(typeof(mean_generic(v))), 17), mean_generic(v))
end
# The helpers that keep a computation in its type.
println("zero(Float32) = ", zero(Float32), ", one(BigFloat) = ", one(BigFloat), ", oftype(1f0, 2) = ", oftype(1f0, 2),
        ", float(Int) = ", float(Int), ", eps(Float32) = ", eps(Float32))
generic_mean.jl, run with Julia 1.13.1 Python · generic_mean_session.py
from session import files, run

# The type each mean returns for five kinds of input.
files("generic_mean.jl")
run("""
julia generic_mean.jl
""")

Output

$ julia generic_mean.jl
input           mean_fixed mean_generic     mean_generic's value
Float32         Float64    Float32          2.6666667
Float64         Float64    Float64          2.6666666666666665
BigFloat        BigFloat   BigFloat         2.666666666666666666666666666666666666666666666666666666666666666666666666666678
Rational{Int64} Float64    Rational{Int64}  1//3
Int64           Float64    Float64          2.3333333333333335
zero(Float32) = 0.0, one(BigFloat) = 1.0, oftype(1f0, 2) = 2.0, float(Int) = Float64, eps(Float32) = 1.1920929e-7

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

The literal 0.0 is a Float64, and every addition promoted the sum to it: Float32 data came back as Float64, and the exact fractions 1//3, 1//6 and 1//2 as a rounded Float64 instead of 1//3. Starting from zero(eltype(v)), each mean stayed in its input’s type. Integer data still gave a Float64, because / between two integers returns a float.

A tolerance written for Float64

Wrong: while abs(dx) > 1e-12. A Float32 step meets it only by becoming exactly zero, since the spacing of Float32 numbers near 1 is 1.1920929e-7, and an iteration that alternates between two neighbouring values never stops. Right: while abs(dx) > 100 * eps(typeof(x)), or a tolerance argument with a default taken from eps(T).

The same functions keep any computation in its type: zero(T) and one(T) for starting values, oftype(x, 2) to write a constant in x’s type, float(T) for the floating-point type that matches T, and eps(T) for a tolerance that fits its precision, 1.1920929e-7 for Float32. Code written this way serves Float32 on a GPU, Float64 for everyday work and BigFloat to check a result to more digits.

Key idea

Write constants through the data’s type, zero(T), one(T), oftype(x, c) and eps(T), and the same code keeps Float32, BigFloat and Rational inputs in their own types.

Checkpoint

Check: Numeric code in the data's own type

  1. One question A mean starts its sum with s = 0.0. What does it return for [1//3, 1//6, 1//2]?

    Choose one answer.

    Show the answer: One question

    Answer: A rounded Float64

    Start from zero(eltype(v)) or zero(T), so the computation stays in the data's type.

Abstract field types cost speed, and type parameters remove the cost

The performance tips name abstract field types as a reason the compiler cannot generate fast code. Two structs hold the same million values and the same scale, one with abstract field types and one with type parameters:

abstract_fields.jl: the same loop over two kinds of struct Julia · abstract_fields.jl
# The same series stored in a struct with abstract field types and in one with type parameters.
struct LooseSeries
    t::AbstractVector
    scale::Real
end
struct TightSeries{V<:AbstractVector{<:Real}, S<:Real}
    t::V
    scale::S
end
function total(s)
    acc = zero(s.scale)
    for x in s.t
        acc += s.scale * x
    end
    return acc
end
data = collect(range(0.0, 1.0; length = 1_000_000))
loose = LooseSeries(data, 2.0)
tight = TightSeries(data, 2.0)
total(loose); total(tight)                     # the first calls compile
println("tight is a ", typeof(tight))
println("same total: ", total(loose) == total(tight))
println("allocated by one call: loose ", @allocated(total(loose)), " bytes, tight ", @allocated(total(tight)), " bytes")
println("inferred result: loose ", only(Base.return_types(total, (LooseSeries,))), ", tight ", only(Base.return_types(total, (typeof(tight),))))
r = TightSeries(1:1_000_000, 2.0)              # a range works too, with its own concrete type
total(r)
println(typeof(r), ": allocated ", @allocated(total(r)), " bytes")
a = TightSeries{AbstractVector{Float64}, Real}(data, 2.0)   # abstract parameters written out by hand
total(a)
println(typeof(a), ": allocated ", @allocated(total(a)), " bytes")
abstract_fields.jl, run with Julia 1.13.1 Python · abstract_fields_session.py
from session import files, run

# A loop over a struct's fields, with abstract field types and with type parameters.
files("abstract_fields.jl")
run("""
julia abstract_fields.jl
""")

Output

$ julia abstract_fields.jl
tight is a TightSeries{Vector{Float64}, Float64}
same total: true
allocated by one call: loose 95991840 bytes, tight 0 bytes
inferred result: loose Any, tight Float64
TightSeries{UnitRange{Int64}, Float64}: allocated 0 bytes
TightSeries{AbstractVector{Float64}, Real}: allocated 95991840 bytes

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

Step through what the compiler knows about each struct when it compiles total:

Interactive · step by step The same data behind abstract fields and behind type parameters Step through what the compiler knows about each struct's fields when it compiles total, and what one call costs.
The same data behind abstract fields and behind type parameters, step 5 of 5. Showing LooseSeries, t::AbstractVector, total(loose), TightSeries{V, S}, t::V, scale::S (Vector{Float64}, Float64), total(tight). In focus: total(tight).declaresread bydeclaresread byLooseSeriesone type for every seriest::AbstractVectorscale::Realtotal(loose)types found at run timeTightSeries{V, S}one type per pair V and St::V, scale::SVector{Float64}, Float64total(tight)types known when compiled

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

Every step as text
  1. Two structs. Both hold the same million Float64 values and the scale 2.0. LooseSeries declares its fields with abstract types; TightSeries takes them from type parameters.
  2. What the struct says. Every LooseSeries has the same type, so its type says only that t is some AbstractVector and scale some Real.
  3. What the parameters say. tight has the type TightSeries{Vector{Float64}, Float64}: the parameters name the exact field types, and each pair of types is its own concrete type.
  4. total on loose. Compiled for LooseSeries, total cannot know the element type or the scale's type, so each step is chosen while the loop runs: 95,991,840 bytes allocated, the result inferred as Any.
  5. total on tight. Compiled for TightSeries{Vector{Float64}, Float64}, the loop works on known Float64 values: 0 bytes allocated, the result inferred as Float64.

The totals were equal, but one call on loose allocated 95,991,840 bytes, about 96 for each value, and its result was inferred only as Any. Every LooseSeries has the same type whatever it holds, so the type tells the compiler nothing about the fields, and each step of the loop was chosen while it ran. tight has the type TightSeries{Vector{Float64}, Float64}, which names the field types, so the loop compiled to arithmetic on known Float64 values and allocated nothing. A range in the same struct made another concrete type, TightSeries{UnitRange{Int64}, Float64}, and allocated nothing either.

The performance tips add a caveat: a parametric struct built with abstract parameters behaves like the loose one. The last line of the run wrote them out by hand, TightSeries{AbstractVector{Float64}, Real}(data, 2.0), and one call allocated 95,991,840 bytes again. Let the constructor read the parameters from the arguments, as TightSeries(data, 2.0) did.

Checkpoint

Check: Abstract field types against type parameters

  1. Question 1 of 2 How many bytes did one call of total(tight) allocate?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 0 bytes

    With the field types in the parameters, the loop compiled to arithmetic on known Float64 values and allocated nothing, against 95,991,840 bytes for loose.

  2. Question 2 of 2 A struct has the field state::AbstractVector. Which change lets code that reads state compile for the actual vector type?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: struct S{V<:AbstractVector}; state::V; end

    A type parameter lets each struct value carry its field types in its own type, which is what the compiler reads.

NTuple puts a length into a type

A tuple’s type lists the type of every position, so its length is part of its type. NTuple{N, T} is short for a tuple of N values of type T, and a method can take N as a parameter:

ntuple.jl: a length in the type Julia · ntuple.jl
# A size in the type: NTuple{N, T} is a tuple of N values of type T.
dot3 = (1.0, 2.0, 3.0)
println(typeof(dot3), " ", typeof(dot3) == NTuple{3, Float64}, " ", sizeof(dot3), " bytes, isbits: ", isbits(dot3))
println(ntuple(i -> i^2, 4))

# N is known when the method is compiled, so mismatched lengths are refused before the code runs.
dot(a::NTuple{N, T}, b::NTuple{N, T}) where {N, T<:Real} = sum(a[i] * b[i] for i in 1:N)
println(dot((1.0, 2.0, 3.0), (4.0, 5.0, 6.0)))
try
    dot((1.0, 2.0, 3.0), (4.0, 5.0))
catch err
    println(first(split(sprint(showerror, err), '\n')))
end
println("allocated by a second call: ", @allocated(dot((1.0, 2.0, 3.0), (4.0, 5.0, 6.0))), " bytes")
ntuple.jl, run with Julia 1.13.1 Python · ntuple_session.py
from session import files, run

# Tuples whose length is part of their type.
files("ntuple.jl")
run("""
julia ntuple.jl
""")

Output

$ julia ntuple.jl
Tuple{Float64, Float64, Float64} true 24 bytes, isbits: true
(1, 4, 9, 16)
32.0
MethodError: no method matching dot(::Tuple{Float64, Float64, Float64}, ::Tuple{Float64, Float64})
allocated by a second call: 0 bytes

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

(1.0, 2.0, 3.0) is an NTuple{3, Float64}: 24 bytes of plain data with no pointers. dot(a::NTuple{N, T}, b::NTuple{N, T}) demands the same N for both arguments, so tuples of 3 and 2 values were refused at dispatch, before any code ran, with a MethodError. N is a constant of the compiled method, so the compiler knows how many times the loop over 1:N runs, and a second call allocated nothing. Packages such as StaticArrays also put the size into the type, for fixed-size vectors and matrices, a subject of the module on fast code.

Not for sizes that change

Encode a length in a type only when it is small and fixed, such as the 3 coordinates of a point in space. Julia compiles a new version of a method for each distinct N, so lengths that vary with the data would compile one version per length.

Checkpoint

Check: A length in the type with NTuple

  1. One question What is the type of (1.0, 2.0, 3.0)?

    Choose one answer.

    Show the answer: One question

    Answer: NTuple{3, Float64}, the same as Tuple{Float64, Float64, Float64}

    A tuple's length is part of its type, which is what lets a method dispatch on it.

Practise: a polynomial type

Polynomial{T}, evaluated by Horner's rule

Write struct Polynomial{T<:Number} holding a coefficient vector, lowest power first, and an outer constructor that promotes mixed coefficients, so Polynomial([1, 2.5]) is a Polynomial{Float64}. Then write (p::Polynomial)(x), which evaluates p at x by Horner’s rule starting from zero, never 0.0. Check on your machine that Polynomial([1//2, 1//3])(3//1) returns the exact 3//2, that a Float32 polynomial at a Float32 point returns a Float32, and that @allocated reports 0 bytes for an evaluation after the first.

Then test yourself on parametric structs, where clauses, generic code, abstract fields and NTuple. The session picks your least secure idea first, and every choice comes with its reason:

Practice that adapts to you

Practise this lesson

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

  1. Question 1 of 11 What type does Point(1//2, 3//4) have?

    Choose one answer.

    Show the answer: Question 1 of 11

    Answer: Point{Rational{Int64}}

    The type parameter takes the type of the fields, so exact fractions stay exact inside the point.

  2. Question 2 of 11 After adding Point(x::Real, y::Real) = Point(promote(x, y)...), what is Point(1, big(2.0))?

    Choose one answer.

    Show the answer: Question 2 of 11

    Answer: Point{BigFloat}(1.0, 2.0)

    promote finds the common type of its arguments, so the outer constructor handles any mix of real numbers.

  3. Question 3 of 11 A function is declared f(x::AbstractVector{T}, y::AbstractVector{T}) where {T}. A user passes an Int64 time axis and Float64 readings. What happens, and what is the usual fix?

    Choose one answer.

    Show the answer: Question 3 of 11

    Answer: A MethodError; accept each vector on its own and promote inside

    Tie types only when the code needs one type; otherwise write f(x::AbstractVector{<:Real}, y::AbstractVector{<:Real}) and use promote_type inside.

  4. Question 4 of 11 What can f(v::AbstractVector{T}) where {T<:Real} do that f(v::AbstractVector{<:Real}) cannot?

    Choose one answer.

    Show the answer: Question 4 of 11

    Answer: Use the name T in its body, for example in zero(T)

    The two signatures match the same calls; the where clause adds a name the body can use. Without it, eltype(v) gives the same type.

  5. Question 5 of 11 Which expression gives a zero of the same type as x, whether x is a Float32, a BigFloat or a Rational?

    Choose one answer.

    Show the answer: Question 5 of 11

    Answer: zero(x)

    zero, one, oftype and eps take a type or a value and answer in that type, which keeps generic code in the data's type.

  6. Question 6 of 11 Why did mean_generic([1, 2, 4]) return a Float64 although it starts from zero(Int64)?

    Choose one answer.

    Show the answer: Question 6 of 11

    Answer: The sum stays Int64, but / between two integers returns a Float64

    Generic code follows each operation's rules: + keeps Int64, / makes a float, // would make a Rational.

  7. Question 7 of 11 A Newton loop written generically stops while abs(dx) > 1e-12. A user runs it in Float32 on a GPU. What happens, and what is the fix?

    Choose one answer.

    Show the answer: Question 7 of 11

    Answer: It may never stop, since Float32 steps near 1 are 1.19e-7; base the tolerance on eps(T)

    Write tolerances through the type, such as 100 * eps(typeof(x)), or take them as an argument with such a default.

  8. Question 8 of 11 Why did total(loose) infer only Any for its result?

    Choose one answer.

    Show the answer: Question 8 of 11

    Answer: Every LooseSeries has the same type, so the type says nothing about what its fields hold

    The compiler reads a value's type, not its contents; abstract field types hide the contents' types.

  9. Question 9 of 11 Someone builds TightSeries{AbstractVector{Float64}, Real}(data, 2.0) explicitly. What did one call of total on it allocate, and why?

    Choose one answer.

    Show the answer: Question 9 of 11

    Answer: 95,991,840 bytes, like the struct with abstract fields, because its parameters are abstract

    Let constructors read the parameters from the arguments, so they are always concrete.

  10. Question 10 of 11 What does dot((1.0, 2.0, 3.0), (4.0, 5.0)) do, with dot(a::NTuple{N, T}, b::NTuple{N, T}) where {N, T<:Real}?

    Choose one answer.

    Show the answer: Question 10 of 11

    Answer: It throws a MethodError before any of its code runs

    Putting the length into the type turns a length mismatch into a dispatch error.

  11. Question 11 of 11 Where is an NTuple{N, Float64} a good choice?

    Choose one answer.

    Show the answer: Question 11 of 11

    Answer: The 3 coordinates of points in a simulation

    Encode a length in a type when it is small and fixed; vectors handle everything else.

Interview questions

Warm-up (fresher to 2–5 years): what does the T in struct Point{T} do?

T is a type parameter: it makes Point a family of types, one for each T, such as Point{Float64} and Point{Rational{Int64}}. Each member is a concrete type whose fields have the type T, so the compiler knows exactly how a Point{Float64} is stored, 16 bytes of plain data, and compiles fast code for it. Julia reads T from the arguments, Point(1.5, 2.0) is a Point{Float64}, and a bound such as T<:Real refuses other types. Point alone is the set of all its members, so a method written for Point accepts every one, while Point{Real} is a different, slower type that Point{Float64} is not a subtype of. Parameters replace abstract field types without losing generality.

Two design questions grow out of this lesson: a simulation struct that allocates in its inner loop, and a numerical routine that changes the type of its results. For each, say which field or constant you would change, then read the first step:

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

  1. Mid to staff

    A simulation keeps its state in struct Sim with the fields state::AbstractVector, dt::Real and params::Dict{Symbol, Any}. Profiling shows the time-step loop allocating on every iteration. Explain why, and redesign the struct without losing the ability to run on Float32 data or on GPU arrays.

    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 numerical routine written with s = 0.0, 1/n and a tolerance of 1e-12 returns Float64 for Float32 inputs, loses exactness for Rational inputs, and never stops for one user's Float32 data. How do you make it generic, and how do you test that it stays generic?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A type parameter makes a family of concrete types: Point(1, 2) is a Point{Int64} and Point(1//2, 3//4) a Point{Rational{Int64}}, and Point alone is a UnionAll of them all.
  • The default constructor of Point{T} needs one T for both fields, so mixed inputs need an outer constructor that calls promote.
  • A where clause names a method’s type parameters; a name used twice ties the arguments to one concrete type, and the body can use the name.
  • Generic numeric code writes constants through the data’s type, so Float32, BigFloat and Rational inputs keep their types instead of falling back to Float64.
  • Abstract field types made one call allocate 95,991,840 bytes and infer Any; type parameters gave the same results with 0 bytes.
  • NTuple{N, T} puts a small, fixed length into a type, so mismatched lengths fail at dispatch, and the run’s dot over 1:N allocated nothing.

Terms in this lesson

parametric type
A type defined with type parameters, such as struct Point{T<:Real}. It stands for a whole family of types, one concrete member for each parameter, and alone it is a UnionAll type.
type parameter
A type written in braces after a type's name, such as T in Point{T}, that the type's fields or a method's arguments refer to. Each value of T gives another type of the family, such as Point{Float64}.
UnionAll type
A type with a parameter left open, written with where or <:, such as Vector{T} where T<:Real, also written Vector{<:Real}. It is the set of every type it covers and is never a value's own type.

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.