Elixir: Functions & Recursion
while.
Named Functions
Named functions live in modules and are identified by name and arity:
summarize/1 and summarize/2 are different functions. Guards and multiple
clauses (see Pattern Matching) attach to the head.
defmodule Stats do
# Private helper — callable only from inside this module.
defp square(n), do: n * n
# Default arguments come from `\\` — the default fallback operator.
def summarize(numbers, label \\ "data", precision \\ 2) do
total = Enum.sum(numbers)
avg = total / length(numbers)
"#{label}: total=#{total}, avg=#{Float.round(avg, precision)}"
end
def rms(numbers), do: numbers |> Enum.map(&square/1) |> mean()
defp mean([]), do: 0.0
defp mean(list), do: Enum.sum(list) / length(list)
end
Stats.summarize([1, 2, 3]) # uses both defaults
Stats.summarize([1, 2, 3], "temps") # uses one default
Default-argument caveat
With defaults, define clauses so that a head-only clause (no body) declares the defaults before the clause bodies — otherwise the compiler warns about a required parameter after an optional one.
def join(list, sep \\ ", ")
def join([], _sep), do: "" # _sep marks it unused on purpose
def join(list, sep), do: Enum.join(list, sep)
Anonymous Functions and Captures
Anonymous functions — fn ... end, or the & capture shorthand — are
values you pass around. They are closures: they remember the bindings from the scope where they
were created.
# Long form: arguments, body, and a `.` call syntax (deliberate).
add = fn a, b -> a + b end
add.(2, 3) #=> 5
# Capture shorthand: & builds a function from an expression.
double = &(&1 * 2) # &1..&n are positional parameters
Enum.map([1, 2, 3], double) #=> [2, 4, 6]
# Capturing an existing named function:
Enum.map([1, 2, 3], &Integer.to_string/1) #=> ["1", "2", "3"]
# Closures: the function remembers `prefix` even after the scope ends.
prefix = "pid-"
tag = &(prefix <> Integer.to_string(&1))
tag.(7) #=> "pid-7"
Why the .? Named and anonymous functions live in different namespaces, and the explicit
call makes it obvious which one you are invoking — run() means the module function,
run.() means the value in the variable.
Recursion and Tail Calls
With immutable data there is no loop counter to mutate, so iteration is recursion: each call consumes the head of the data and recurses on the tail. The BEAM optimizes tail calls — a recursive call as the very last action — into a jump that reuses the stack frame, so the loop runs in constant stack space.
defmodule Rec do
# BODY recursion: the addition happens AFTER the recursive call returns.
# Elegant but keeps one stack frame per element.
def sum_body([]), do: 0
def sum_body([h | t]), do: h + sum_body(t)
# TAIL recursion: the accumulator carries the result and the recursive
# call is the last thing — constant stack, any list length.
def sum_tail(list), do: do_sum(list, 0)
defp do_sum([], acc), do: acc
defp do_sum([h | t], acc), do: do_sum(t, acc + h)
end
Rec.sum_body([1, 2, 3]) #=> 6
Rec.sum_tail(Enum.to_list(1..1_000_000)) #=> fine — no stack overflow
Higher-Order Thinking
Once functions are values, patterns like map, filter, and reduce replace most hand-written
recursion. Write reduce by hand once — usually as a tail-recursive function with an
accumulator — and afterwards prefer Enum.reduce/3 and friends for clarity.
# Word frequencies the reduce way — the accumulator is the result so far.
"fun to learn fun to ship"
|> String.split()
|> Enum.reduce(%{}, fn word, acc ->
Map.update(acc, word, 1, &(&1 + 1))
end)
#=> %{"fun" => 2, "to" => 2, "learn" => 1, "ship" => 1}
Practice
- Write both
sum_body/1andsum_tail/1; try them on a one-million-element list and observe which one survives. - Rewrite
Stats.rms/1without the private helper, using only captures and pipes. - Implement
my_map/2over a list using tail recursion, then compare it withEnum.map/2.