Intermediate

Functions in Erlang

Learn how to define and call functions in Erlang - multiple clauses, guards, recursion, and first-class funs - with Docker-ready escript examples

Functions are not just a feature of Erlang - they are the entire program. As a functional language, Erlang has no statements, loops, or mutable variables to fall back on. Everything you do is expressed as a function that takes arguments and returns a value. Understanding functions deeply is the key to thinking the Erlang way.

What makes Erlang’s functions distinctive is how much work happens in the function head rather than the body. A single function name can have many clauses, and Erlang chooses the right one by pattern matching the arguments and checking guards. There is no if ladder at the top of most functions - the branching is built into how the function is declared. Because data is immutable and there are no for or while loops, recursion is how repetition gets done.

Functions are also first-class values. You can bind a function to a variable, pass it to another function, return it from a function, and capture surrounding variables in a closure. This makes higher-order functions like lists:map/2 and lists:foldl/3 everyday tools rather than advanced tricks.

In this tutorial you will learn how to define and call functions, use multiple clauses and guards, write both body-recursive and tail-recursive functions, and work with anonymous functions (fun), higher-order functions, and closures. All examples run as escript files using the official erlang:alpine image.

Defining and Calling Functions

A function is defined by its name, a parameter list, the -> arrow, and a body. The body is a sequence of expressions separated by commas, and the value of the last expression is what the function returns - there is no return keyword. The most powerful idea is that one function can have several clauses (separated by ;), and Erlang selects the clause whose pattern matches the arguments. Adding a when guard refines a clause with a boolean condition.

Create a file named functions.erl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env escript
%% Defining and calling functions in Erlang

main(_) ->
    %% Calling a simple, single-clause function
    io:format("square(9) = ~p~n", [square(9)]),

    %% A function that takes several parameters
    io:format("add(3, 4) = ~p~n", [add(3, 4)]),

    %% Multiple clauses: the matching one is chosen by argument shape
    io:format("area of circle r=2: ~p~n", [area({circle, 2})]),
    io:format("area of rectangle 3x4: ~p~n", [area({rectangle, 3, 4})]),

    %% Guards (the `when` keyword) add a boolean test to a clause
    io:format("sign(-8) = ~p~n", [sign(-8)]),
    io:format("sign(0) = ~p~n", [sign(0)]),
    io:format("sign(15) = ~p~n", [sign(15)]).

%% A single-clause function. The body's last expression is the return value.
square(N) -> N * N.

add(A, B) -> A + B.

%% One function name, two clauses matched on the shape of the argument.
%% Clauses are separated by ';' and the function ends with '.'
area({circle, R})       -> 3.14159 * R * R;
area({rectangle, W, H}) -> W * H.

%% Guards restrict when a clause applies. They are tried top to bottom.
sign(N) when N < 0 -> negative;
sign(0)            -> zero;
sign(N) when N > 0 -> positive.

Notice that area/1 takes a single tuple and inspects its tag (circle or rectangle) to decide what to compute. This pattern - tagging data with an atom and matching on it - is everywhere in idiomatic Erlang.

Recursion Instead of Loops

Erlang has no for or while loops. Repetition is expressed with recursion: a function calls itself with smaller or changed arguments until it reaches a base case. List recursion uses the [Head | Tail] pattern to peel off one element at a time.

There are two common shapes. A body-recursive function (like the classic factorial) builds its result as the call stack unwinds. A tail-recursive function carries the result along in an accumulator argument, so the recursive call is the very last thing it does - the BEAM optimizes this into a loop that runs in constant stack space, which matters for long-running and deeply recursive code.

Create a file named recursion.erl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/usr/bin/env escript
%% Recursion replaces loops in Erlang

main(_) ->
    %% Body-recursive factorial
    io:format("factorial(6) = ~p~n", [factorial(6)]),

    %% Recursing over a list with [Head | Tail]
    io:format("sum([1,2,3,4,5]) = ~p~n", [sum([1, 2, 3, 4, 5])]),
    io:format("len([a,b,c,d]) = ~p~n", [len([a, b, c, d])]),

    %% Tail recursion: an accumulator avoids growing the stack
    io:format("factorial_tail(6) = ~p~n", [factorial_tail(6)]),

    io:format("count_down(5): ", []),
    count_down(5),
    io:format("~n", []).

%% Body-recursive: the multiply happens after the recursive call returns
factorial(0)            -> 1;
factorial(N) when N > 0 -> N * factorial(N - 1).

%% Walk a list: base case is the empty list, then handle head + tail
sum([])    -> 0;
sum([H|T]) -> H + sum(T).

len([])    -> 0;
len([_|T]) -> 1 + len(T).

%% Tail-recursive: a public function delegates to a private helper/2
factorial_tail(N) -> factorial_tail(N, 1).

factorial_tail(0, Acc)            -> Acc;
factorial_tail(N, Acc) when N > 0 -> factorial_tail(N - 1, N * Acc).

%% Recursion can also drive side effects, like printing
count_down(0) -> io:format("liftoff!");
count_down(N) when N > 0 ->
    io:format("~p ", [N]),
    count_down(N - 1).

Both factorial/1 and factorial_tail/1 return the same answer, but the tail-recursive version is the one you would reach for in production code. The two-clause factorial_tail (arity 1 and arity 2) shows a common idiom: a clean public function that sets up the accumulator for a private worker function.

First-Class Functions, Higher-Order Functions, and Closures

In Erlang, functions are values. An anonymous function - called a fun - is written fun(Args) -> Body end and can be bound to a variable or passed around. Higher-order functions like lists:map/2, lists:filter/2, and lists:foldl/3 take a fun as an argument and apply it across a collection. You can also refer to a named function as a value with the fun Name/Arity syntax.

A closure is a fun that captures variables from the scope where it was created. Because Erlang data is immutable, a closure safely “remembers” those values forever.

Create a file named higher_order.erl:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env escript
%% Functions are first-class values in Erlang

main(_) ->
    %% An anonymous function (a "fun") bound to a variable
    Triple = fun(X) -> X * 3 end,
    io:format("Triple(7) = ~p~n", [Triple(7)]),

    %% Pass funs to higher-order library functions
    Doubled = lists:map(fun(X) -> X * 2 end, [1, 2, 3, 4]),
    io:format("map double: ~p~n", [Doubled]),

    Evens = lists:filter(fun(X) -> X rem 2 =:= 0 end, [1, 2, 3, 4, 5, 6]),
    io:format("filter evens: ~p~n", [Evens]),

    %% foldl reduces a list to a single value using an accumulator
    Total = lists:foldl(fun(X, Acc) -> X + Acc end, 0, [1, 2, 3, 4, 5]),
    io:format("foldl sum: ~p~n", [Total]),

    %% Reference a named function as a value with `fun Name/Arity`
    Shouts = lists:map(fun shout/1, [hi, there]),
    io:format("map shout: ~p~n", [Shouts]),

    %% A closure captures N from the surrounding scope
    AddTen = make_adder(10),
    io:format("AddTen(5) = ~p~n", [AddTen(5)]).

shout(Atom) ->
    string:uppercase(atom_to_list(Atom)).

%% Returns a fun that "remembers" N - a closure
make_adder(N) ->
    fun(X) -> X + N end.

make_adder/1 is the key example: it returns a brand-new function each time it is called, and that returned function still has access to the N it was built with. This is how Erlang achieves partial application and configurable behavior without any mutable state.

Running with Docker

You can run all three examples with the official Erlang image - no local install required.

1
2
3
4
5
6
7
# Pull the official Erlang image
docker pull erlang:alpine

# Run each example with escript
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript functions.erl
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript recursion.erl
docker run --rm -v $(pwd):/app -w /app erlang:alpine escript higher_order.erl

Expected Output

Running functions.erl:

square(9) = 81
add(3, 4) = 7
area of circle r=2: 12.56636
area of rectangle 3x4: 12
sign(-8) = negative
sign(0) = zero
sign(15) = positive

Running recursion.erl:

factorial(6) = 720
sum([1,2,3,4,5]) = 15
len([a,b,c,d]) = 4
factorial_tail(6) = 720
count_down(5): 5 4 3 2 1 liftoff!

Running higher_order.erl:

Triple(7) = 21
map double: [2,4,6,8]
filter evens: [2,4,6]
foldl sum: 15
map shout: ["HI","THERE"]
AddTen(5) = 15

Key Concepts

  • Functions are identified by name and arity - factorial_tail/1 and factorial_tail/2 are different functions, which makes the “public wrapper + private helper” idiom natural.
  • Multiple clauses replace branching - one function name can have many clauses; Erlang picks the first whose pattern matches the arguments, so the dispatch logic lives in the function head.
  • Guards (when) add conditions - use them for tests pattern matching alone cannot express, such as N > 0; clauses are tried top to bottom.
  • No return keyword - a function returns the value of the last expression in its body; expressions are separated by commas.
  • Recursion replaces loops - peel lists apart with [Head | Tail], and prefer tail recursion with an accumulator for long-running work so the BEAM runs it in constant stack space.
  • Functions are first-class values - bind a fun to a variable, pass it to lists:map/2 and friends, or reference a named function with fun Name/Arity.
  • Closures capture immutable state - a fun remembers the variables from where it was defined, enabling partial application without mutable variables.
  • Punctuation matters - commas separate expressions, semicolons separate clauses, and a period ends the whole function definition.

Running Today

All examples can be run using Docker:

docker pull erlang:alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining