Anonymous Functions with ->
An anonymous function has no bound name and is written with the arrow syntax x -> x^2, or for multiple arguments, (x, y) -> x + y; it's a first-class value that can be stored in a variable, passed to another function, or called immediately. They're most commonly used as the first argument to higher-order functions like map, filter, and sort, e.g., filter(x -> x % 2 == 0, 1:20) keeps only even numbers without needing a separately named helper function.
Cricket analogy: A one-off ad-hoc fielding instruction shouted mid-over — 'you, cover point, now' — doesn't need a name or a formal role assignment to be understood and acted on immediately, similar to how x -> x^2 defines a function inline without needing a name.
The arrow syntax also supports multiple statements using parentheses and semicolons or a begin...end block: x -> (y = x^2; y + 1) is valid, though anything beyond a couple of expressions usually reads better as a do-block or a small named function.
Passing Functions as Arguments and do-blocks
Because functions are first-class values in Julia, they can be passed directly as arguments: map(x -> x^2, [1, 2, 3]) applies the anonymous function to every element, and sort(words, by = x -> length(x)) sorts by a computed key rather than the elements' natural order. Named functions work identically in these positions — map(sqrt, data) — so the anonymous form is really just a convenience for one-off transformations that aren't worth naming separately.
Cricket analogy: A team can apply the same fielding-restriction rule to every over in the powerplay uniformly, whether that rule is the standard one or a custom one agreed for that match, mirroring how map(f, collection) applies any function, named or anonymous, to every element uniformly.
When an anonymous function's body spans multiple lines, the do block syntax is more readable than a bulky inline ->: open("data.txt") do io; for line in eachline(io); process(line); end; end is sugar for passing the anonymous function created by the do block as the *first* argument to open. This pattern is why resource-handling functions like open, and testing helpers like @testset, are commonly written with do — it reads like a natural block of code rather than a callback squeezed onto one line.
Cricket analogy: A pre-match team briefing given as a full structured talk in the dressing room reads far more naturally than trying to shout the entire game plan in one breath at the toss, similar to how a do-block spreads a multi-line function body naturally instead of squeezing it into one inline arrow expression.
Closures: Capturing Variables from Enclosing Scope
A closure is a function that captures variables from the scope it was defined in, keeping them alive even after that scope has returned — function make_counter(); n = 0; return () -> (n += 1); end returns a function that increments and remembers its own private n across calls, and each call to make_counter() creates a fresh, independent n. This is how Julia implements stateful callbacks and simple generators without needing a class, since the returned function 'closes over' its own copy of the enclosing variable.
Cricket analogy: A personal scorer assigned to track one specific batter's running tally across an entire innings keeps that batter's own private running total independent of every other batter's count, mirroring how each call to make_counter() creates its own independent captured n.
Performance Considerations for Closures
Closures that capture a variable which is later reassigned in the enclosing scope — especially a loop variable — can suffer a performance penalty because Julia 'boxes' the captured variable (wraps it in a mutable, type-unstable container) to allow the closure to see updates. A common fix inside a loop that creates closures is wrapping the captured value in a let block, e.g., fns = [let n = i; () -> n; end for i in 1:3], which creates a fresh, non-reassigned binding per iteration so the compiler can keep it unboxed and fast.
Cricket analogy: If a scoreboard operator has to keep re-checking a shared, constantly-updated master tally sheet instead of writing down a fixed number the moment it's confirmed, that lookup overhead slows things down, similar to how a boxed captured variable forces slower, indirect access compared to a fixed value baked in via let.
# Anonymous functions passed to higher-order functions
evens = filter(x -> x % 2 == 0, 1:20)
lengths_sorted = sort(["kiwi", "fig", "banana"], by = x -> length(x))
# do-block sugar for a multi-line anonymous function
open("data.txt") do io
for line in eachline(io)
println(uppercase(line))
end
end
# Closures: each call captures its own independent n
function make_counter()
n = 0
return () -> (n += 1)
end
counter_a = make_counter()
counter_b = make_counter()
counter_a() # 1
counter_a() # 2
counter_b() # 1 (independent state)
# let block avoids boxing when closures are built inside a loop
fns = [let n = i; () -> n; end for i in 1:3]
map(f -> f(), fns) # [1, 2, 3], each closure keeps its own captured n
A closure created inside a for loop that directly captures the loop variable can end up sharing a single mutated binding across all the closures in some nested-scope patterns — always verify with a quick test (e.g., map(f -> f(), fns) after building fns in a loop) that each closure actually captured the value you expected, and use a let block when in doubt.
- Anonymous functions use x -> expr syntax and are first-class values, most often passed to map, filter, and sort.
- Named and anonymous functions are interchangeable as arguments; the anonymous form is just a naming convenience.
- do-block syntax is sugar for passing a (usually multi-line) anonymous function as the first argument to a call.
- A closure captures variables from its defining scope and keeps them alive after that scope returns.
- Each invocation of a function that returns a closure creates a fresh, independent captured binding.
- Captured variables reassigned in a loop can be boxed, hurting performance; wrap with let to force a fresh binding.
- let n = i; () -> n; end inside a loop comprehension is the idiomatic fix for closure-over-loop-variable issues.
Practice what you learned
1. What does filter(x -> x % 2 == 0, 1:20) return?
2. What is the do-block syntax sugar for?
3. In function make_counter(); n = 0; return () -> (n += 1); end, what happens if you call make_counter() twice and use each returned function independently?
4. Why might a closure that captures a loop variable be slower than expected?
5. What does wrapping a loop's captured variable in a let block typically fix?
Was this page helpful?
You May Also Like
Functions in Julia
Learn Julia's function syntax, multiple return values, keyword and optional arguments, and the mutating-function naming convention.
Loops in Julia
Master for and while loops, iteration over collections, break/continue, and performance-minded loop patterns in Julia.
Multiple Dispatch Explained
Understand how Julia selects a method based on the runtime types of all arguments, and why this is central to Julia's design and performance.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics