Functions in depth: keyword arguments, do blocks and closures
Design a function's signature, distinguish positional from keyword arguments, pass behaviour with do blocks, and understand what a closure captures.
Signature design
# positional: part of the identity of the call
# keyword: named options with defaults, after the semicolon
function fit(x, y; method = :ls, tol = 1e-8, maxiter = 1000)
(; method, tol, maxiter)
end
fit(1:10, 1:10) # all keywords default
fit(1:10, 1:10; method = :huber) # one keyword overridden
# varargs
total(args...) = sum(args)
count_types(xs...) = length(xs)
print_all(io, xs...) = foreach(x -> println(io, x), xs)
# optional positional arguments are really extra methods
greet(name) = "hello " * name
greet(name, title) = "hello " * title * " " * name- Default positional arguments are not a separate feature: each default creates another method.
- Keyword arguments are gathered into a
NamedTuple, so they add a small allocation and are not part of dispatch. - Dispatch happens on positional arguments only. Two methods that differ only in keyword names are the same method.
do blocks and anonymous functions
map(x -> x^2, 1:5) # anonymous
map(x -> x^2, 1:5) |> sum
# do syntax: the block becomes the FIRST argument
open("data.txt", "w") do io
write(io, "hello\n")
end
# equivalent to
open(io -> write(io, "hello\n"), "data.txt", "w")
sort([3, 1, 2]; by = x -> -x) # a named keyword taking a function
filter(iseven, 1:10) # a function passed positionallyThe do form exists because the first argument is where callback-taking functions put their function. Any function whose first parameter is a function can be called with a trailing block, which is why open, map and lock all read naturally.
Closures and scope
function make_counter()
n = 0
() -> (n += 1) # the closure captures n by reference
end
c = make_counter()
c(); c()
# let introduces a new binding, which is what you want inside loops
funcs = [let i = i; () -> i; end for i in 1:3]
[ f() for f in funcs ] # 1, 2, 3 with let; 3, 3, 3 without it (in older versions)
# a captured variable that is reassigned becomes a boxed Ref: a performance trap
function slow_accumulate(v)
total = 0
for x in v
total += x
end
total
end💡
A closure that assigns to a captured variable forces Julia to box that variable, which allocates and can slow a hot loop by a large factor. If a loop variable is only read, there is no box; if it is written, keep the accumulation in the function's own scope rather than inside a nested closure.
FAQ
Should a function use keyword arguments for everything optional?
Use keywords for options that do not change what the function fundamentally does, such as tolerances and verbosity. Use positional arguments for things that are part of its identity, and add methods rather than flags when the behaviour differs substantially.
Why does my do block not compile?
The function being called must accept a function as its first positional argument. If it takes the callback last, or as a keyword, wrap the call yourself instead of using
do.Related
Multiple dispatch Control flow, loops and comprehensions
Last refreshed 2026-09-18.