Intermediate

Functions in Elixir

Learn how to define and use functions in Elixir - named and anonymous functions, multiple clauses, guards, recursion, and higher-order functions with Docker-ready examples

In Elixir, functions are the fundamental unit of code. As a functional language, Elixir treats functions as first-class values: you can bind them to variables, pass them as arguments, return them from other functions, and store them in data structures. There are no objects with methods here—just functions that transform immutable data into new data.

Elixir has two kinds of functions. Named functions live inside modules and are defined with def. Anonymous functions are values you can create on the fly with fn or the capture operator &. Understanding the difference between them—and how they are called—is essential.

Because Elixir is built on the Erlang VM and embraces immutability, it has no traditional loops. Iteration is expressed through recursion and higher-order functions like Enum.map/2. Pattern matching in function heads and guards let you write multiple clauses that respond to different inputs without if statements. This tutorial walks through each of these ideas with runnable examples.

Named Functions

Named functions must be defined inside a module using def (public) or defp (private). A function can use a full do...end block, or the do: shorthand for one-liners. The last expression in the body is returned automatically—there is no return keyword.

Create a file named functions_basic.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule Calculator do
  # Multi-line function using a do...end block
  def add(a, b) do
    a + b
  end

  # Single-line function using the do: shorthand
  def square(n), do: n * n
end

IO.puts(Calculator.add(3, 4))
IO.puts(Calculator.square(5))

Functions are identified by both their name and their arity (number of arguments), written as add/2 or square/1. Two functions with the same name but different arity are completely separate functions.

Default Parameters

Elixir supports default argument values using the \\ operator. If the caller omits an argument, the default is used.

Create a file named default_params.exs:

1
2
3
4
5
6
7
8
9
defmodule Greeter do
  # \\ provides a default value for the greeting parameter
  def greet(name, greeting \\ "Hello") do
    "#{greeting}, #{name}!"
  end
end

IO.puts(Greeter.greet("World"))
IO.puts(Greeter.greet("Elixir", "Welcome"))

The first call uses the default "Hello", while the second supplies its own greeting.

Anonymous Functions and the Capture Operator

Anonymous functions are values created with fn ... end. Note that calling them requires a dot before the parentheses: double.(10), not double(10). This explicit dot syntax distinguishes anonymous function calls from named function calls. The capture operator & provides a compact shorthand, and can also capture existing named functions so they can be passed around as values.

Create a file named anonymous.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Anonymous function bound to a variable; called with a dot
double = fn x -> x * 2 end
IO.puts(double.(10))

# The capture operator & is shorthand: &1 is the first argument
triple = &(&1 * 3)
IO.puts(triple.(10))

# Capturing a named function (String.upcase/1) as a value
upcase = &String.upcase/1
IO.puts(upcase.("elixir"))

Anonymous functions are closures: they capture the variables in scope at the point where they are defined. Each function has its own local scope—variables defined inside one function clause are not visible to others.

Recursion with Multiple Clauses and Guards

Without loops, Elixir uses recursion for repeated work. Recursion combines naturally with multiple function clauses and guards (the when keyword). Elixir tries each clause from top to bottom and runs the first one whose pattern and guard match.

Create a file named recursion.exs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
defmodule Math do
  # Base case: factorial of 0 is 1
  def factorial(0), do: 1

  # Recursive case with a guard ensuring n is positive
  def factorial(n) when n > 0 do
    n * factorial(n - 1)
  end
end

IO.puts(Math.factorial(5))
IO.puts(Math.factorial(10))

The base case (factorial(0)) stops the recursion, while the guarded clause handles every positive integer. This pattern—a base case plus a recursive case—is the functional replacement for an imperative loop.

Higher-Order Functions and the Pipe Operator

Because functions are values, you can pass them to other functions. The Enum module is full of such higher-order functions, and the pipe operator |> chains them by feeding the result of one call as the first argument to the next.

Create a file named higher_order.exs:

1
2
3
4
5
6
7
8
# Each Enum function takes another function as an argument
result =
  1..5
  |> Enum.map(fn x -> x * x end)
  |> Enum.filter(fn x -> rem(x, 2) == 1 end)
  |> Enum.sum()

IO.puts("Sum of odd squares from 1..5: #{result}")

Reading top to bottom: 1..5 produces [1, 2, 3, 4, 5], Enum.map/2 squares each to [1, 4, 9, 16, 25], Enum.filter/2 keeps the odd values [1, 9, 25], and Enum.sum/1 adds them to 35. The pipe operator turns nested function calls into a readable, left-to-right pipeline.

Running with Docker

Run each example with the official Elixir image—no local install required:

1
2
3
4
5
6
7
8
9
# Pull the official image
docker pull elixir:1.17-alpine

# Run each example
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir functions_basic.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir default_params.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir anonymous.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir recursion.exs
docker run --rm -v $(pwd):/app -w /app elixir:1.17-alpine elixir higher_order.exs

Expected Output

7
25
Hello, World!
Welcome, Elixir!
20
30
ELIXIR
120
3628800
Sum of odd squares from 1..5: 35

Key Concepts

  • Functions are first-class values — bind them to variables, pass them as arguments, and return them from other functions.
  • Named vs. anonymous — named functions (def) live in modules and are called like Calculator.add(3, 4); anonymous functions (fn/&) are called with a dot: double.(10).
  • Arity matters — a function is identified by name and number of arguments (factorial/1); same name with different arity means a different function.
  • Implicit return — the last expression in a function body is its return value; there is no return keyword.
  • Multiple clauses and guards — pattern matching in the function head plus when guards replace many if/switch statements.
  • Recursion replaces loops — a base case plus a recursive case is the idiomatic way to iterate in a functional language.
  • The capture operator & — a concise shorthand for anonymous functions (&(&1 * 3)) and a way to turn named functions into values (&String.upcase/1).
  • Higher-order functions and |> — functions like Enum.map/2 accept other functions, and the pipe operator chains transformations into readable pipelines.

Running Today

All examples can be run using Docker:

docker pull elixir:1.17-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining