Elixir: Functions & Recursion

Elixir has two kinds of functions — named and anonymous — and one loop construct: recursion. This lesson covers both kinds, the capture syntax you will meet in every pipeline, and why tail recursion makes functional loops as cheap as 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.

Comparison of body recursion growing the stack versus a tail call with an accumulator reusing the frame
Fig. 1 — Body recursion keeps every frame until the end; a tail call with an accumulator reuses one frame.
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

  1. Write both sum_body/1 and sum_tail/1; try them on a one-million-element list and observe which one survives.
  2. Rewrite Stats.rms/1 without the private helper, using only captures and pipes.
  3. Implement my_map/2 over a list using tail recursion, then compare it with Enum.map/2.

Next: Collections & Enumeration