Intermediate

Functions in Gleam

Learn how to define and use functions in Gleam - parameters, labelled arguments, anonymous functions, higher-order functions, and recursion with Docker-ready examples

Functions are the heart of Gleam. As a functional language, Gleam treats functions as first-class values: you can store them in variables, pass them as arguments, and return them from other functions. There are no methods, no classes, and no objects - just functions operating on immutable data.

Gleam’s functions are also where its type system shines. Every parameter and return value has a type, but thanks to type inference you rarely need to write those types out by hand. When you do annotate them, the annotations double as documentation that the compiler verifies. And because Gleam has no return keyword and no mutable state, functions are pure expressions that always produce a value.

In this tutorial you’ll learn how to define and call functions, use Gleam’s distinctive labelled arguments, work with anonymous functions and closures, write higher-order functions, and use recursion - which replaces the loops you’d reach for in imperative languages.

Defining and Calling Functions

A function is defined with the fn keyword. Add pub to make it visible outside the module. Parameters and the return value can be annotated with types, but the compiler infers them when you leave them off. There is no return statement - the value of the last expression is what the function returns.

Create a file named functions_basic.gleam:

 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
import gleam/int
import gleam/io

// A function with typed parameters and a return type
pub fn add(a: Int, b: Int) -> Int {
  a + b
}

// Type annotations are optional; the compiler infers them here
pub fn double(x) {
  x * 2
}

// The last expression is the return value - no `return` keyword
fn square(x: Int) -> Int {
  x * x
}

pub fn main() {
  let sum = add(3, 4)
  let doubled = double(10)
  let squared = square(5)

  io.println("3 + 4 = " <> int.to_string(sum))
  io.println("double(10) = " <> int.to_string(doubled))
  io.println("square(5) = " <> int.to_string(squared))
}

A few things to notice:

  • pub fn vs fn - pub exposes the function to other modules; a plain fn is module-private.
  • -> declares the return type. Functions defined later in the file (like square) can be called from earlier ones - order doesn’t matter within a module.
  • <> is the string concatenation operator, and int.to_string converts an Int to a String for printing.

This program prints:

3 + 4 = 7
double(10) = 20
square(5) = 25

Labelled Arguments

Gleam lets you give parameters labels so call sites are self-documenting. A labelled parameter has two parts: the label used by callers and the internal name used inside the function body. Labelled arguments can also be supplied in any order, which removes the “which argument was which?” guesswork common to languages with positional-only parameters.

Create a file named functions_labels.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import gleam/int
import gleam/io

// `from` and `subtract` are the labels; `a` and `b` are the names
// used inside the function body.
fn subtract(from a: Int, subtract b: Int) -> Int {
  a - b
}

pub fn main() {
  // Call sites read like a sentence
  let r1 = subtract(from: 10, subtract: 3)

  // Labelled arguments can be given in any order
  let r2 = subtract(subtract: 3, from: 10)

  io.println(int.to_string(r1))
  io.println(int.to_string(r2))
}

Both calls produce the same result because the labels - not the position - decide which value goes where:

7
7

Labelled arguments are Gleam’s answer to named and keyword parameters. The standard library uses them heavily (for example string.replace(in:, each:, with:)) to make code read naturally.

Anonymous Functions, Closures, and Function Capture

Functions are values, so you can create them inline without naming them. An anonymous function uses fn(...) { ... }. When it references variables from the surrounding scope, it becomes a closure that captures them. Gleam also offers a shorthand called function capture: writing _ in place of an argument turns a call into a new function.

Create a file named functions_higher_order.gleam:

 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
import gleam/int
import gleam/io
import gleam/list

// A higher-order function: it takes another function as a parameter
fn apply_twice(f: fn(Int) -> Int, value: Int) -> Int {
  f(f(value))
}

fn add(a: Int, b: Int) -> Int {
  a + b
}

pub fn main() {
  // An anonymous function bound to a variable
  let increment = fn(x: Int) -> Int { x + 1 }
  io.println(int.to_string(apply_twice(increment, 5)))

  // A closure captures `factor` from the surrounding scope
  let factor = 3
  let scale = fn(x: Int) -> Int { x * factor }
  io.println(int.to_string(scale(10)))

  // Function capture: `add(2, _)` becomes a one-argument function
  let add_two = add(2, _)
  io.println(int.to_string(add_two(8)))

  // Pass an anonymous function to a standard-library higher-order function
  let doubled = list.map([1, 2, 3], fn(x) { x * 2 })
  io.println(int.to_string(int.sum(doubled)))
}

Walking through main:

  • apply_twice(increment, 5) applies increment twice: 5 -> 6 -> 7.
  • scale closes over factor, so scale(10) is 10 * 3 = 30.
  • add(2, _) captures the first argument, producing a function where add_two(8) is 2 + 8 = 10.
  • list.map applies the anonymous function to every element, giving [2, 4, 6], and int.sum adds them to 12.
7
30
10
12

The type fn(Int) -> Int in apply_twice is a function type - it describes a function taking an Int and returning an Int. This is what makes higher-order functions type-safe.

Recursion

Gleam has no for or while loops. Instead, repetition is expressed with recursion: a function that calls itself. Pattern matching with case defines the base case (when to stop) and the recursive case (how to break the problem down). Because Gleam runs on the BEAM, tail-recursive functions - where the recursive call is the last thing the function does - are optimized into iteration and won’t grow the stack.

Create a file named functions_recursion.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import gleam/int
import gleam/io

// Classic recursion: the base case stops, the recursive case shrinks `n`
fn factorial(n: Int) -> Int {
  case n {
    0 -> 1
    _ -> n * factorial(n - 1)
  }
}

// Tail-recursive sum using an accumulator. The BEAM optimizes this into
// a loop, so it stays safe even on long lists.
fn sum_list(numbers: List(Int), total: Int) -> Int {
  case numbers {
    [] -> total
    [first, ..rest] -> sum_list(rest, total + first)
  }
}

pub fn main() {
  io.println("5! = " <> int.to_string(factorial(5)))
  io.println("sum = " <> int.to_string(sum_list([1, 2, 3, 4, 5], 0)))
}

The sum_list function matches on the shape of the list: [] is the empty list (base case), and [first, ..rest] destructures the head element and the remaining tail. Each call passes a running total forward as an accumulator.

5! = 120
sum = 15

A Comprehensive Example

This program brings the concepts together: a basic function, labelled arguments, an anonymous function passed to a higher-order function, and the pipe operator chaining calls.

Create a file named functions.gleam:

 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
import gleam/int
import gleam/io
import gleam/list

// Named function with typed parameters and return type
fn add(a: Int, b: Int) -> Int {
  a + b
}

// Labelled arguments, with recursion to compute a power
fn power(base base: Int, exponent exp: Int) -> Int {
  case exp {
    0 -> 1
    _ -> base * power(base: base, exponent: exp - 1)
  }
}

// Higher-order function: accepts a function as an argument
fn transform(numbers: List(Int), with f: fn(Int) -> Int) -> List(Int) {
  list.map(numbers, f)
}

pub fn main() {
  // Calling a basic function
  io.println("add(2, 3) = " <> int.to_string(add(2, 3)))

  // Labelled arguments make the intent obvious
  io.println("2^10 = " <> int.to_string(power(base: 2, exponent: 10)))

  // Anonymous function passed to a higher-order function
  let tripled = transform([1, 2, 3], fn(x) { x * 3 })
  io.println("tripled sum = " <> int.to_string(int.sum(tripled)))

  // The pipe operator chains function calls top-to-bottom
  let result =
    [1, 2, 3, 4]
    |> list.filter(fn(x) { x % 2 == 0 })
    |> int.sum
  io.println("even sum = " <> int.to_string(result))
}

The pipe operator (|>) passes the result of each expression as the first argument to the next, so the final block reads as “take this list, keep the even numbers, then sum them.”

Running with Docker

Gleam uses a project-based build system, so the command below creates a throwaway project, copies your source file into it, and runs it - all in one step. Use the exact image from the language overview.

1
2
3
4
5
# Pull the official Gleam image (includes the Erlang runtime)
docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine

# Create a project, copy your file in, and run it
docker run --rm -v $(pwd):/work ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine sh -c "gleam new hello --skip-git > /dev/null 2>&1 && cp /work/functions.gleam hello/src/hello.gleam && cd hello && gleam run"

To run any of the other examples, swap functions.gleam for that file’s name (for example functions_recursion.gleam).

Expected Output

Running functions.gleam produces:

add(2, 3) = 5
2^10 = 1024
tripled sum = 18
even sum = 6

Key Concepts

  • Functions are first-class values - store them in variables, pass them as arguments, and return them, all checked by the type system using function types like fn(Int) -> Int.
  • No return keyword - a function returns the value of its last expression, keeping functions as pure expressions.
  • Type inference - parameter and return type annotations are optional; the compiler infers them, but annotations serve as verified documentation.
  • Labelled arguments - parameters can have a caller-facing label and an internal name, making call sites self-documenting and order-independent.
  • Closures and function capture - anonymous functions capture surrounding variables, and the _ shorthand turns a partial call into a new function.
  • Higher-order functions - functions that take or return other functions power standard-library tools like list.map and list.filter.
  • Recursion replaces loops - with no for or while, repetition is expressed through recursion and pattern matching; tail-recursive functions are optimized by the BEAM.
  • The pipe operator (|>) - chains function calls into readable top-to-bottom data transformations.

Running Today

All examples can be run using Docker:

docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining