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 3 – Control flow, functions and scope

Functions, arguments and keyword arguments

Write Julia functions in long, short and anonymous forms, with defaults, required keywords, varargs, splatting, tuple returns and in-place versions.

  • Beginner
  • 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 functions in long, short and anonymous forms, including do-blocks
  • Use optional, keyword, required-keyword and variadic arguments and splatting
  • Annotate argument types for dispatch without shutting out Float32 or BigFloat
  • Use tuples to return several values, and destructure them
  • Follow the ! convention for functions that mutate their arguments

Before you start

What you’ll learn · Before you start

What you will learn

  • Define functions in long, short and anonymous forms, including do-blocks
  • Use optional, keyword, required-keyword and variadic arguments and splatting
  • Annotate argument types for dispatch without shutting out Float32 or BigFloat
  • Use tuples to return several values, and destructure them
  • Follow the ! convention for functions that mutate their arguments

Before you start

On this page

A call such as deflection(5000, 2.0, 200e9, 8.356e-5) computes the tip deflection of a steel cantilever, but nothing in it says which number is the load, the length, the stiffness of the steel or the shape of the beam. Name the last two, give the steel’s stiffness as a default, and the same call reads deflection(5000, 2.0; I = 8.356e-5): 0.8 mm for an IPE 300 beam 2 m long with 5 kN at its tip.

This lesson builds that function and its neighbours, recorded on Julia 1.13.1. You will write functions in their three forms and with do-blocks, give arguments defaults, require keywords, accept any number of arguments, return several values at once, and name the functions that change their arguments so that every caller can tell.

A function can be written three ways, and a do-block passes one

The same function, the area of a rectangle, can be written in three forms. The long form uses function … end and may hold any number of lines; the short form, area_short(w, h) = w * h, fits one expression; an anonymous function, (w, h) -> w * h, has no name of its own and is usually passed straight to another function:

forms.jl: three forms of one function, then two do-blocks Julia · forms.jl
# One function written three ways, then a do-block, which passes a function as the first argument of a call.
function area_long(w, h)
    return w * h
end
area_short(w, h) = w * h
area_anon = (w, h) -> w * h
println(area_long(3, 4), " ", area_short(3, 4), " ", area_anon(3, 4))

spans = [2.0, 3.5, 1.25]
println(map(x -> 2x, spans))
doubled = map(spans) do x
    2x
end
println(doubled)

open("spans.txt", "w") do io          # open closes the file when the block ends, even after an error
    for s in spans
        println(io, s, " m")
    end
end
println(readlines("spans.txt"))
forms.jl, run with Julia 1.13.1 Python · forms_session.py
from session import files, run

# The long, short and anonymous forms of one function, then two do-blocks.
files("forms.jl")
run("""
julia forms.jl
""")

Output

$ julia forms.jl
12 12 12
[4.0, 7.0, 2.5]
[4.0, 7.0, 2.5]
["2.0 m", "3.5 m", "1.25 m"]

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

All three printed 12. A function returns the value of its last expression unless a return ends it earlier, so the long form’s return is a choice of style; it makes the result easy to find in a function of many lines.

The second half of the program is the reason anonymous functions are everywhere in Julia. map(x -> 2x, spans) hands the function to map, which calls it on every item. When that function needs several lines, a do-block: Syntax that writes a multi-line anonymous function after a call and passes it as the call's first argument: map(v) do x … end is map(x -> …, v). writes it after the call instead. Step through what Julia makes of the two blocks:

Interactive · step by step What a do-block turns into Step through the rewrite. On the left the block as written, on the right the call that Julia makes of it.
What a do-block turns into, step 5 of 5. Showing map(spans) do x, body: 2x, x -> 2x, map(x -> 2x, spans), open(path, "w") do io, open(f, path, "w"). In focus: open(path, "w") do io, open(f, path, "w").becomesits bodygoes firstbecomesmap(spans) do xa callbody: 2xup to the matching endx -> 2xan anonymous functionmap(x -> 2x, spans)passed as the first argumentopen(path, "w") do ioa block that writes a fileopen(f, path, "w")closes the file after fbecomes

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

Every step as text
  1. The block. A do-block follows a call: map(spans) do x. The name after do, x, is the parameter of a function that has no name.
  2. Its body. The lines up to the matching end are that function's body. Here the body is 2x, so the function doubles its argument.
  3. An anonymous function. Julia turns the block into the anonymous function x -> 2x.
  4. Passed first. It passes that function as the first argument of the call, ahead of spans: map(x -> 2x, spans). Both forms printed [4.0, 7.0, 2.5].
  5. The same for open. open(path, "w") do io … end is the same rewrite. open receives a function of io, opens the file, calls the function and closes the file when it returns, even after an error.

The open block is the pattern you will use most: the file is closed when the block ends, which the manual says happens whether the function exits normally or throws an exception. The run read the file back as three lines.

return inside a do-block

Wrong: return inside a do-block to leave the function that contains it. The block is a function of its own, so return leaves only the block and the outer function goes on. Right: compute a value in the block and return it from the outer function after the call, or use a loop where an early exit is the point.

A default value adds a method, and a type annotation narrows it

area(w, h = w) gives h a default, so the function can be called with one argument or two. The manual explains how: optional arguments are only a convenient syntax for writing several method definitions. Each 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” is one definition for one way of calling the function, and methods(area) lists them:

optional.jl: a default value, a narrow annotation and a general one Julia · optional.jl
# A default value for a positional argument: Julia writes one method per way of calling the function.
area(w, h = w) = w * h               # a rectangle, or a square when h is left out
println(area(3, 4), " ", area(3))
println(methods(area))

half_narrow(x::Float64) = x / 2      # accepts only Float64
half(x::Real) = x / 2                # accepts every real number
println(half(1.5), " ", half(1.5f0), " ", half(big"1.5"), " ", half(3))
println(typeof(half(1.5f0)), " ", typeof(half(big"1.5")))
half_narrow(1.5f0)
optional.jl, run with Julia 1.13.1 Python · optional_session.py
from session import files, run

# A default value makes two methods; a narrow type annotation shuts out a Float32.
files("optional.jl")
run("""
!julia optional.jl
""")

Output

$ julia optional.jl
12 9
# 2 methods for generic function "area" from Main:
 [1] area(w, h)
     @ ~/demo/optional.jl:2
 [2] area(w)
     @ ~/demo/optional.jl:2
0.75 0.75 0.75 1.5
Float32 BigFloat
ERROR: LoadError: MethodError: no method matching half_narrow(::Float32)
The function `half_narrow` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  half_narrow(!Matched::Float64)
   @ Main ~/demo/optional.jl:6

Stacktrace:
 [1] top-level scope
   @ ~/demo/optional.jl:10
 [2] include(mod::Module, _path::String)
   @ Base ./Base.jl:309
 [3] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:344
 [4] _start()
   @ Base ./client.jl:577
in expression starting at ~/demo/optional.jl:10
[exit status 1]

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

area(3, 4) gave 12 and area(3) gave 9, the area of a square, and methods(area) listed two methods, area(w, h) and area(w). The one-argument method simply calls the other with h = w. An optional argument: A positional argument with a default value, as h in area(w, h = w). It is still matched by position, and Julia writes one method for each number of arguments the function accepts. is still matched by position: to give h, you must give w first.

The second half annotates argument types. half_narrow(x::Float64) accepts only a Float64, so the last line, with the Float32 value 1.5f0, stopped with MethodError: no method matching half_narrow(::Float32), and the message listed the closest candidate, the method that did not match. half(x::Real) accepted a Float64, a Float32, a BigFloat and an Int, and kept the type of each float: half(1.5f0) is a Float32 and half(big"1.5") a BigFloat, while half(3) gave the Float64 1.5, because / on integers returns a float.

Narrow annotations buy no speed. The manual says that argument-type declarations normally have no impact on performance, because Julia compiles a specialised version of a function for the concrete types it is called with, annotated or not. Annotations are for choosing a method, for documentation and for refusing input that would give nonsense; the style guide warns against overly specific types, and the manual’s advice is “when in doubt, omit the argument types”.

Checkpoint

Check: Function forms and do-blocks · Defaults, methods and type annotations

  1. Question 1 of 2 Which call is the same as map([1, 2, 3]) do x; x + 1; end?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: map(x -> x + 1, [1, 2, 3])

    A do-block is another way to write an anonymous function as the first argument of a call: f(args) do x … end means f(x -> …, args), as both forms of forms.jl printed [4.0, 7.0, 2.5].

  2. Question 2 of 2 How many methods does volume(a, b = a, c = b) = a * b * c define?

    Type a number.

    A hint

    Count the ways to call it, with one, two or three arguments.

    Show the answer: Question 2 of 2

    Answer: 3 methods

    Optional arguments are a short way of writing several methods: volume(a, b, c), volume(a, b) and volume(a), one for each number of arguments, as area(w, h = w) made two.

Keywords name the arguments, and one without a default is required

The tip of a cantilever with a load F at its unsupported end deflects by F·L³ / (3·E·I), the formula the beam deflection calculator uses too, where L is the length, E the stiffness of the material (Young’s modulus) and I the second moment of area of the beam’s section. E is the same for every beam of one material, so the function gives it a default, 200 GPa for steel, while I depends on the shape of the section and has none; the examples use 8.356e-5 m⁴, the value the moment of inertia calculator gives for an IPE 300. So deflection takes F and L by position and E and I as keyword argument: An argument declared after a semicolon and passed by name, as I in deflection(F, L; E = 200e9, I). A keyword without a default must be given, or the call throws UndefKeywordError., after a semicolon, with a default for E only:

deflection.jl: a documented function with keyword arguments Julia · deflection.jl
"""
    deflection(F, L; E = 200e9, I)

Tip deflection, in metres, of a cantilever of length `L` metres with a load of `F` newtons at its free end:
F * L^3 / (3 * E * I). `E` is Young's modulus in pascals (200 GPa, steel, unless given) and `I` the second moment of
area in m^4, which has no default.
"""
deflection(F, L; E = 200e9, I) = F * L^3 / (3 * E * I)
cantilever.jl: calls with keywords, then one without I Julia · cantilever.jl
# Keyword arguments: E has a default, I has none, so every call must name I.
include("deflection.jl")

I_ipe300 = 8.356e-5                  # an IPE 300 steel section about its strong axis: 8,356 cm^4
mm(x) = round(x * 1000; digits = 2)
println("steel:      ", mm(deflection(5_000, 2.0; I = I_ipe300)), " mm")
println("E = 70 GPa: ", mm(deflection(5_000, 2.0; E = 70e9, I = I_ipe300)), " mm")
println("any order:  ", deflection(5_000, 2.0; I = I_ipe300, E = 200e9) == deflection(5_000, 2.0; I = I_ipe300))
I = I_ipe300
println("shorthand:  ", mm(deflection(5_000, 2.0; I)), " mm")   # ; I means I = I
deflection(5_000, 2.0)

Follow the first call as Julia binds its arguments:

Interactive · stage by stage How one call fills the four arguments Step down the column. Each stage binds one kind of argument, and the body runs only once all four have a value.
How one call fills the four arguments, step 7 of 7. Showing a call with one keyword, F = 5000, L = 2.0, E = 200e9, I = 8.356e-5, F * L^3 / (3 * E * I), 7.98e-4 m. In focus: I = 8.356e-5.a call with one keywordI = 8.356e-5; E left outF = 5000, L = 2.0positional: by their orderE = 200e9not given: its defaultI = 8.356e-5given by nameF * L^3 / (3 * E * I)the body7.98e-4 mprinted as 0.8 mmno I: UndefKeywordError

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

Every step as text
  1. The call. deflection(5_000, 2.0; I = I_ipe300) passes two arguments by position and one by name. The semicolon separates them, and E is not mentioned at all.
  2. By position. Positional arguments are matched by their order: F = 5000 and L = 2.0. Swapped, they would still run and give a wrong number.
  3. A default. E was not given, so its default, 200e9 pascals for steel, is used. A default is evaluated only for a keyword the call leaves out.
  4. By name. I is matched by its name, wherever it stands among the keywords. It has no default, so every call must give it.
  5. The body. With all four bound, the body computes 5000 × 2.0³ / (3 × 200e9 × 8.356e-5).
  6. The result. The answer is 7.98e-4 m, which the run printed as 0.8 mm. The call with E = 70e9 gave 2.28 mm.
  7. Without I. A call that leaves I out stops before the body runs: UndefKeywordError: keyword argument I not assigned, as the last line of the run shows.

Here is what the run of cantilever.jl printed, line by line:

cantilever.jl, run with Julia 1.13.1 Python · cantilever_session.py
from session import files, run

# Keyword arguments with a default and a required one, called four ways and then without the required keyword.
files("deflection.jl", "cantilever.jl")
run("""
!julia cantilever.jl
""")

Output

$ julia cantilever.jl
steel:      0.8 mm
E = 70 GPa: 2.28 mm
any order:  true
shorthand:  0.8 mm
ERROR: LoadError: UndefKeywordError: keyword argument `I` not assigned
Stacktrace:
 [1] deflection(F::Int64, L::Float64)
   @ Main ~/demo/deflection.jl:8
 [2] top-level scope
   @ ~/demo/cantilever.jl:11
 [3] include(mod::Module, _path::String)
   @ Base ./Base.jl:309
 [4] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:344
 [5] _start()
   @ Base ./client.jl:577
in expression starting at ~/demo/cantilever.jl:11
[exit status 1]

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

Steel gave 0.8 mm, and a softer material with E = 70e9 gave 2.28 mm. The keywords could come in any order, and ; I was short for I = I, the variable of the same name. The last call left I out and stopped with UndefKeywordError: keyword argument `I` not assigned: a keyword without a default is required, and Julia says which one is missing.

The text in triple quotes above the definition is a docstring. It must sit directly above the definition, with nothing between them, and its first line is the signature, indented by four spaces. Typing ? at the REPL prompt turns it into help mode, which shows it:

The docstring of deflection, in the REPL's help mode Python · docstring_session.py
from session import files, repl

# The docstring above deflection, shown by the REPL's help mode.
files("deflection.jl")
repl([
    'include("deflection.jl")',
    "?deflection",
])

Output

$ julia --startup-file=no -q
julia> include("deflection.jl")
deflection (generic function with 1 method)

help?> deflection
search: deflection function @cfunction selectdim delete! setprecision

  deflection(F, L; E = 200e9, I)

  Tip deflection, in metres, of a cantilever of length L metres with a load of
  F newtons at its free end: F * L^3 / (3 * E * I). E is Young's modulus in
  pascals (200 GPa, steel, unless given) and I the second moment of area in
  m^4, which has no default.

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

Beam Deflection Calculator Check the 0.8 mm: a 2 m cantilever with 5 kN at the tip, on an IPE 300 section. Moment of Inertia & Section Properties Calculator Work out I for your own section, then pass it to deflection as a keyword.

Varargs gather arguments, and splatting spreads them

A function written total(xs...) accepts any number of arguments: the manual calls them varargs: A function that accepts any number of arguments, written with ... after the last parameter, as total(xs...). Inside the function, xs is a tuple of the arguments passed, empty when there are none., and inside the function xs is a tuple of whatever was passed, empty when nothing was. ... in a call does the opposite, splatting: Writing ... after a collection in a call, as total(v...), so that its elements are passed as separate arguments instead of one.: total([4, 5]...) spreads the vector into two separate arguments.

varargs.jl: varargs, splatting and keywords passed on Julia · varargs.jl
# xs... collects any number of arguments into a tuple; v... in a call spreads a collection into arguments.
total(xs...) = sum(xs; init = 0)
println(total(1, 2, 3), " ", total(), " ", total([4, 5]...))
total_no_init(xs...) = sum(xs)       # the same without init
try
    total_no_init()
catch e
    println("total_no_init(): ", sprint(showerror, e))
end

describe(first, rest...) = println("first ", first, ", then ", length(rest), " more: ", rest)
describe(10, 20, 30)

include("deflection.jl")
# Every load, the same keywords: kwargs... collects them, and ; kwargs... passes them on.
deflections(loads...; kwargs...) = [round(deflection(F, 2.0; kwargs...) * 1000; digits = 2) for F in loads]
println(deflections(1_000, 5_000, 10_000; I = 8.356e-5), " mm")
println(deflections(5_000; E = 70e9, I = 8.356e-5), " mm")
varargs.jl, run with Julia 1.13.1 Python · varargs_session.py
from session import files, run

# Varargs and splatting, a sum of no arguments without init, then keywords collected with kwargs... and passed on.
files("deflection.jl", "varargs.jl")
run("""
julia varargs.jl
""")

Output

$ julia varargs.jl
6 0 9
total_no_init(): ArgumentError: reducing over an empty collection is not allowed; consider supplying `init` to the reducer
first 10, then 2 more: (20, 30)
[0.16, 0.8, 1.6] mm
[2.28] mm

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

What each call of the run received:

Call Inside the function Result
total(1, 2, 3) xs = (1, 2, 3) 6
total() xs = () 0, from init = 0
total([4, 5]...) xs = (4, 5) 9
total_no_init() xs = () ArgumentError
describe(10, 20, 30) first = 10, rest = (20, 30) “2 more”

total() needed sum(xs; init = 0): an empty tuple has no first value to start a sum from, and total_no_init(), the same function without init, threw ArgumentError: reducing over an empty collection is not allowed. Keywords can be gathered the same way: deflections(loads...; kwargs...) collects every keyword it is given and passes them all on with ; kwargs..., so one I served three loads, 0.16, 0.8 and 1.6 mm, and E = 70e9 reached deflection without deflections knowing about it. This is how a function wraps another without repeating its keyword list.

Checkpoint

Check: Keyword arguments and required keywords · Varargs, splatting and kwargs

  1. Question 1 of 2 With deflection(F, L; E = 200e9, I) = F * L^3 / (3 * E * I), what does deflection(5_000, 2.0; E = 70e9) do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws UndefKeywordError: keyword argument I not assigned

    A keyword without a default is required. The call stops before the body runs and names the missing keyword, as the last line of cantilever.jl did.

  2. Question 2 of 2 total(xs...) = sum(xs; init = 0) and v = [4, 5, 6]. What is total(v...), and what is xs inside that call?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 15, with xs = (4, 5, 6)

    Splatting in a call and varargs in a definition are opposites: v... turns a collection into separate arguments, xs... collects separate arguments into a tuple.

Several results come back as one tuple

A function returns one value, but that value can be a tuple. spread(v) returns the mean, the standard deviation and the count of five days of rainfall as return m, s, n, and the caller takes the tuple apart:

returns.jl: a tuple of results, taken apart four ways Julia · returns.jl
# Several results come back as one tuple, which the caller takes apart.
function spread(v)
    n = length(v)
    m = sum(v) / n
    s = sqrt(sum((x - m)^2 for x in v) / (n - 1))
    return m, s, n                   # a tuple of three values
end

rain = [12.0, 0.0, 33.5, 7.25, 18.0] # five days of rainfall in mm
r = spread(rain)
println(r, " :: ", typeof(r))
m, s, n = spread(rain)
println("mean ", m, ", std ", round(s; digits = 3), ", n ", n)
m, _, n = spread(rain)               # _ drops a value you do not need
lo, hi = extrema(rain)
println("driest ", lo, ", wettest ", hi)

stats(v) = (mean = sum(v) / length(v), n = length(v))   # a named tuple
(; n, mean) = stats(rain)            # take fields by name, in any order
println("n = ", n, ", mean = ", mean)
returns.jl, run with Julia 1.13.1 Python · returns_session.py
from session import files, run

# A function that returns a tuple, and four ways to take the result apart.
files("returns.jl")
run("""
julia returns.jl
""")

Output

$ julia returns.jl
(14.15, 12.66589909955073, 5) :: Tuple{Float64, Float64, Int64}
mean 14.15, std 12.666, n 5
driest 0.0, wettest 33.5
n = 5, mean = 14.15

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

The whole result printed as (14.15, 12.66589909955073, 5) of type Tuple{Float64, Float64, Int64}. Taking it apart into names is destructuring: Taking a tuple or another collection apart into several names in one assignment, as m, s, n = spread(v), or taking fields of a named tuple by name with (; n, mean) = stats(v)., and the run used four forms of it:

  • m, s, n = spread(rain) binds the three values by position.
  • m, _, n = spread(rain) drops the middle one: the manual says a name made only of underscores can be assigned but never read.
  • lo, hi = extrema(rain) works on any function that returns a tuple; it gave 0.0 and 33.5.
  • (; n, mean) = stats(rain) takes fields of a named tuple by their names, in any order, so the caller cannot mix up two numbers of the same type.

Return a named tuple, (mean = …, n = …), when the results are more than two or easy to confuse; a plain tuple is enough for a pair such as extrema.

A function may change your array, and its name should say so

Julia passes arguments by sharing: the manual says values are not copied, so a function that writes into an array it was given changes the caller’s array. The style guide’s rule is that such a function’s name ends in !, as sort! and push! do, and that it usually returns the changed object. The convention is often paired with a version that copies first and leaves its argument alone:

normalize.jl: normalize! and normalize, sharing one loop Julia · normalize.jl
# Two versions of one operation: normalize! changes the vector it is given, normalize leaves it alone.
function normalize!(v)
    lo, hi = extrema(v)
    for i in eachindex(v)
        v[i] = (v[i] - lo) / (hi - lo)
    end
    return v
end
normalize(v) = normalize!(float.(v)) # a new Float64 vector, then the same code

raw = [10.0, 15.0, 30.0]
scaled = normalize(raw)
println("normalize:  ", scaled, ", raw is still ", raw)
same = normalize!(raw)
println("normalize!: ", raw, ", and the result is raw itself: ", same === raw)

counts = [0, 5, 10]
println("normalize(counts) = ", normalize(counts))
normalize!(counts)                   # an Int vector cannot hold 0.5
normalize.jl, run with Julia 1.13.1 Python · normalize_session.py
from session import files, run

# The copying and the in-place versions of one operation, then the in-place one on a vector of integers.
files("normalize.jl")
run("""
!julia normalize.jl
""")

Output

$ julia normalize.jl
normalize:  [0.0, 0.25, 1.0], raw is still [10.0, 15.0, 30.0]
normalize!: [0.0, 0.25, 1.0], and the result is raw itself: true
normalize(counts) = [0.0, 0.5, 1.0]
ERROR: LoadError: InexactError: Int64(0.5)
Stacktrace:
 [1] Int64
   @ ./float.jl:923 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] setindex!
   @ ./array.jl:1019 [inlined]
 [4] normalize!(v::Vector{Int64})
   @ Main ~/demo/normalize.jl:5
 [5] top-level scope
   @ ~/demo/normalize.jl:19
 [6] include(mod::Module, _path::String)
   @ Base ./Base.jl:309
 [7] exec_options(opts::Base.JLOptions)
   @ Base ./client.jl:344
 [8] _start()
   @ Base ./client.jl:577
in expression starting at ~/demo/normalize.jl:19
[exit status 1]

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

normalize(raw) returned [0.0, 0.25, 1.0] and left raw as it was; normalize!(raw) turned raw itself into [0.0, 0.25, 1.0] and returned it, same === raw. The copying version is one line, normalize!(float.(v)), so the arithmetic exists once. Step through what the two versions did with the integer vector counts:

Interactive · step by step The copying and the in-place call on integers Step through both calls. On the left the copying version, on the right the in-place one, on the same vector.
The copying and the in-place call on integers, step 7 of 7. Showing normalize(counts), float.(counts), normalize!(the copy), returns the copy, normalize!(counts), counts[1] = 0.0, counts[2] = 0.5, InexactError. In focus: counts[2] = 0.5, InexactError.normalize(counts)the copying versionfloat.(counts)a new Vector{Float64}normalize!(the copy)writes 0.0, 0.5, 1.0returns the copycounts is unchangednormalize!(counts)the in-place versioncounts[1] = 0.0stored as the Int 0counts[2] = 0.5no Int64 has this valueInexactErrorInt64(0.5) is refused

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

Every step as text
  1. The copying call. normalize(counts) is called with the integer readings [0, 5, 10].
  2. A new vector. float.(counts) makes a new Vector{Float64}, [0.0, 5.0, 10.0]. counts itself is not touched.
  3. The shared code. normalize! runs on the copy and writes 0.0, 0.5 and 1.0 into it: the arithmetic is written once, in the in-place version.
  4. The result. The copy is returned. The run printed [0.0, 0.5, 1.0], and counts kept its values.
  5. The in-place call. normalize!(counts) writes into the vector it was given, and that vector holds Int64 values.
  6. The first value. The first result, (0 − 0) / (10 − 0) = 0.0, converts to the integer 0 without loss, so it is stored.
  7. The second value. The second result is 0.5, and no Int64 has that value. An in-place function can store only values of the element type its argument already has.

float.(v) is the copy, and it also changes the element type, so normalize(counts) worked on integers. The in-place call could not: a Vector{Int64} cannot hold 0.5, and the run stopped with InexactError: Int64(0.5).

An in-place function on the wrong element type

Wrong: calling normalize! on a vector of integer counts, or copy(v) as the copy in normalize, which keeps the integers. Right: copy with float.(v), as here, or write the in-place version for vectors of floats only, and let its ! warn callers that their array changes.

Key idea

A ! at the end of a name promises that the function changes an argument; pair it with a copying version that calls it, so the code is written once and callers choose.

Checkpoint

Check: Returning and destructuring tuples · The ! convention and in-place functions

  1. Question 1 of 2 A function ends with return (mean = m, n = length(v)). Which line takes the count into n and the mean into mean, whatever the order of the fields?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: (; n, mean) = f(v)

    (; a, b) = x takes the fields a and b of x by name. Use a named tuple when results are easy to confuse.

  2. Question 2 of 2 After raw = [10.0, 15.0, 30.0]; same = normalize!(raw), which is true?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: raw is now [0.0, 0.25, 1.0] and same === raw

    A function whose name ends in ! changes an argument, and by convention it returns the changed object, so same and raw are one vector.

Practise: keyword arguments and in-place functions

Add a keyword with a default

Copy deflection.jl and cantilever.jl, add a keyword units = :m to deflection that returns millimetres when it is :mm, add a call with units = :mm to cantilever.jl and run julia cantilever.jl. That call should print about 0.79783, the 0.8 mm of the run before rounding, and every call that leaves units out should print what it printed before.

These questions draw on the runs above, starting with the idea your answers show you know least; every choice explains itself as soon as you pick it:

Practice that adapts to you

Practise this lesson

18 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 18 What does function f(x) x + 1; x * 2 end return for f(3)?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: 6, the value of the last expression

    A function returns its last expression's value, or the value of a return that ends it early. x + 1 is computed and thrown away.

  2. Question 2 of 18 Which call sorts the strings ["pune", "goa", "mumbai"] by their length?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: sort(["pune", "goa", "mumbai"]; by = s -> length(s))

    An anonymous function is passed where a function is expected, here as the by keyword. by = length works too, since length is already a function.

  3. Question 3 of 18 Inside function first_long(words), a line foreach(words) do w; length(w) > 5 && return w; end is meant to return the first long word. What does first_long return?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: Not that word, because return leaves only the do-block's own function

    Use a loop, for w in words; length(w) > 5 && return w; end, or findfirst, when an early exit is the point. The manual's docstring of return warns about exactly this.

  4. Question 4 of 18 area(w, h = w) = w * h. How does Julia know which argument area(3) is?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: By position, the first argument is w, and h takes its default

    area(3) runs the method area(w), which calls area(3, 3), so it gave 9 in the run.

  5. Question 5 of 18 Why did half_narrow(1.5f0) fail while half(1.5f0) returned a Float32?

    Choose one answer.

    Show the answer: Question 5 of 18

    Answer: half_narrow accepts only Float64, and 1.5f0 is a Float32; half accepts any Real

    An argument annotation restricts which calls a method accepts. Use the widest type that still makes sense, such as Real or Number, or none at all.

  6. Question 6 of 18 A colleague annotates every argument as ::Float64 "to make the code fast". What do you tell them?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: Julia compiles a specialised version for the concrete types it is called with anyway, so the annotation adds no speed and shuts out other types

    Annotate for dispatch, documentation or to refuse nonsense, with types as general as the code allows. The style guide warns against overly specific types.

  7. Question 7 of 18 In f(x, y; scale = 1, shift), which arguments are keywords?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: scale and shift, everything after the semicolon

    Keywords follow the semicolon. A keyword with a default is optional, and one without, like shift, must be given by name in every call.

  8. Question 8 of 18 In cantilever.jl, I = I_ipe300 and then deflection(5_000, 2.0; I). What does ; I mean?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: I = I: the keyword I gets the value of the variable I

    f(x; I) is f(x; I = I). It keeps calls short when a variable already has the keyword's name.

  9. Question 9 of 18 A function needs the length of a beam, which every call has, and the material stiffness, which is usually steel. How should the two be declared?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: The length positional, the stiffness a keyword with a default for steel

    Positional for the few inputs every call needs; keywords for settings, with a default when one value is usual and without one when no value is safe to assume, as I was.

  10. Question 10 of 18 Inside describe(first, rest...), called as describe(10, 20, 30), what is rest?

    Choose one answer.

    Show the answer: Question 10 of 18

    Answer: The tuple (20, 30)

    x... at the end of a definition collects the remaining positional arguments into a tuple, which is empty when there are none.

  11. Question 11 of 18 What does ; kwargs... do in deflections(loads...; kwargs...) = [deflection(F, 2.0; kwargs...) for F in loads]?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: It collects every keyword of the call and passes them all on to deflection

    kwargs... lets a wrapper accept the keywords of the function it calls without listing them, so new keywords of deflection work through deflections at once.

  12. Question 12 of 18 longest(words...) = maximum(length, words) gives the length of the longest word it is passed. What does longest() do, and what is the fix?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: It throws an ArgumentError, because there is no value to start from; write maximum(length, words; init = 0)

    A varargs function must handle the empty tuple. Give the reduction an init, maximum(length, words; init = 0), or start with a guard such as isempty(words) && return 0.

  13. Question 13 of 18 What type did spread(rain) return in the run?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: Tuple{Float64, Float64, Int64}

    Several results come back as one tuple, which the caller destructures, as m, s, n = spread(rain) did.

  14. Question 14 of 18 After m, _, n = spread(rain), what happens if the next line is println(_)?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: It is an error, because a name made only of underscores can be assigned but never read

    _ is the way to skip a value while destructuring: it says to the reader that the value is not needed.

  15. Question 15 of 18 A function returns a minimum, a maximum, a mean and a standard deviation, all Float64. What is the safest return value for its callers?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: A named tuple, so callers take the fields by name

    A plain tuple suits a pair such as extrema; with several results of the same type, a named tuple lets callers destructure by field.

  16. Question 16 of 18 What does a ! at the end of a function name, as in sort!, tell the caller?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: That the function changes one of its arguments

    sort(v) returns a sorted copy and sort!(v) sorts v itself. The ! is a promise to the reader, not a rule the compiler enforces.

  17. Question 17 of 18 Why did normalize(counts) work on the integer vector [0, 5, 10] while normalize!(counts) failed?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: normalize copies with float.(v), so its vector can hold 0.5; normalize! writes into the Int vector, which cannot

    An in-place function stores into its argument, so the argument's element type must hold the results: InexactError: Int64(0.5) said that it could not.

  18. Question 18 of 18 You need both a copying and an in-place version of a smoothing function. How do you write them so the arithmetic exists once?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: Write smooth! with the loop, and smooth(v) = smooth!(float.(v))

    The pattern of Base, sort and sort!, applies to your own code: the in-place version holds the work, and the copying one copies and delegates.

Interview questions

Warm-up (fresher to 2–5 years): how does an optional positional argument differ from a keyword argument in Julia?

An optional positional argument has a default but is still matched by its position: area(w, h = w) can be called as area(3) or area(3, 4), and Julia writes one method for each way of calling it, two here, as methods(area) listed. A keyword argument comes after a semicolon in the definition and is matched by name: deflection(F, L; E = 200e9, I) takes I = 8.356e-5 in any position among the keywords, and a keyword without a default, such as I, must be given, or the call throws UndefKeywordError. Positional arguments suit the few inputs every call needs, in an order everyone expects; keywords suit settings that a reader should see named at the call. Keywords also play no part in choosing which method runs.

Two harder questions follow, about loosening an over-narrow signature and about a function whose positional arguments callers keep swapping. Decide your answer first, then open each first step:

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

  1. Fresher to senior

    A colleague's scale!(v::Vector{Float64}, k::Float64) works, but it fails for Float32 data, for an integer factor and for a view of a matrix column. How do you loosen the signature, and how do you show that the change costs no speed?

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

  2. Mid to senior

    A simulation function takes eight positional parameters, and two call sites passed the time step and the tolerance in each other's places. How would you redesign its signature, keep the old calls working for one release, and stop the mix-up from coming back?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A function has a long form, a short form and an anonymous form, and a do-block passes a multi-line anonymous function as the first argument of a call, as open(path, "w") do io … end does.
  • A default value for a positional argument makes one method for each way of calling, two for area(w, h = w), and a type annotation chooses methods without making the code faster, so x::Float64 only shuts out other types such as Float32.
  • Keyword arguments follow a semicolon and are matched by name; a keyword without a default is required, and leaving it out throws UndefKeywordError.
  • xs... gathers any number of arguments into a tuple, v... in a call spreads a collection into arguments, and kwargs... passes keywords on to another function.
  • Several results come back as a tuple or a named tuple, taken apart by position, with _ for a value you skip, or by field name with (; n, mean) = ….
  • A function whose name ends in ! changes an argument, which the caller shares; a copying version such as normalize(v) = normalize!(float.(v)) keeps the arithmetic in one place.

Terms in this lesson

destructuring
Taking a tuple or another collection apart into several names in one assignment, as m, s, n = spread(v), or taking fields of a named tuple by name with (; n, mean) = stats(v).
do-block
Syntax that writes a multi-line anonymous function after a call and passes it as the call's first argument: map(v) do x … end is map(x -> …, v).
keyword argument
An argument declared after a semicolon and passed by name, as I in deflection(F, L; E = 200e9, I). A keyword without a default must be given, or the call throws UndefKeywordError.
optional argument
A positional argument with a default value, as h in area(w, h = w). It is still matched by position, and Julia writes one method for each number of arguments the function accepts.
splatting
Writing ... after a collection in a call, as total(v...), so that its elements are passed as separate arguments instead of one.
varargs
A function that accepts any number of arguments, written with ... after the last parameter, as total(xs...). Inside the function, xs is a tuple of the arguments passed, empty when there are none.

References

Related tools

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.