Beginner

Control Flow in Gleam

Learn control flow in Gleam — the case expression that replaces if/else and switch, plus recursion and list functions in place of loops

Control flow decides which code runs and how often. Most imperative languages reach for if/else statements and for/while loops to do this. Gleam, being a small and consistent functional language, takes a more focused approach: there is one branching construct — the case expression — and there are no loops at all.

This surprises newcomers, but it is deliberate. Gleam has no if statement, no switch, no for, and no while. Instead, case does all the branching through pattern matching, and repetition is expressed with recursion or with functions from the standard library’s list module. Because every value is immutable, there is no mutable loop counter to increment — you describe a transformation and let the language carry it out.

A key consequence is that case is an expression: it evaluates to a value you can bind with let or pass to a function. The compiler also checks that your patterns are exhaustive, so if you forget to handle a possible case it tells you at compile time rather than failing at runtime.

In this tutorial you’ll learn how case replaces if/else and switch, how guards add conditions to patterns, how to destructure tuples and lists while matching, and how recursion and list functions take the place of traditional loops.

Conditionals with case

Since Gleam has no if statement, two-way and multi-way branching are both written with case. Guards — introduced with the if keyword inside a pattern — let a branch match only when an extra condition holds. The wildcard _ is the catch-all.

Create a file named conditionals.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
import gleam/io
import gleam/int

pub fn main() {
  let temperature = 18

  // Guards (the `if` after a pattern) turn `case` into a multi-way branch
  let description = case temperature {
    t if t < 0 -> "freezing"
    t if t < 15 -> "cold"
    t if t < 25 -> "comfortable"
    _ -> "hot"
  }
  io.println(int.to_string(temperature) <> " degrees is " <> description)

  // `case` is an expression, so its result can be bound with `let`
  let number = -7
  let sign = case number {
    n if n > 0 -> "positive"
    n if n < 0 -> "negative"
    _ -> "zero"
  }
  io.println(int.to_string(number) <> " is " <> sign)

  // Matching on a Bool is how you write a simple two-way decision
  let age = 20
  let status = case age >= 18 {
    True -> "adult"
    False -> "minor"
  }
  io.println("Age " <> int.to_string(age) <> ": " <> status)
}

Patterns are tried top to bottom, so order matters: temperature is 18, which fails < 0 and < 15 but matches < 25, giving “comfortable”. Because case returns a value, the sign and status bindings capture the result of the branch that matched — there is no separate ternary operator because case already fills that role.

Pattern Matching and Destructuring

The real power of case shows when you match the shape of data. Alternative patterns with | let one branch cover several values, and you can destructure tuples and lists directly in the pattern, binding their parts to names.

Create a file named pattern_matching.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
41
42
43
import gleam/io
import gleam/int

pub fn main() {
  // Alternative patterns with `|` match several values in one branch
  let day = "Sat"
  let kind = case day {
    "Sat" | "Sun" -> "weekend"
    _ -> "weekday"
  }
  io.println(day <> " is a " <> kind)

  // Matching a tuple destructures several values at once
  let point = #(0, 5)
  let location = case point {
    #(0, 0) -> "origin"
    #(0, _) -> "on the y-axis"
    #(_, 0) -> "on the x-axis"
    #(x, y) -> "at " <> int.to_string(x) <> ", " <> int.to_string(y)
  }
  io.println(location)

  // The spread pattern `..` splits a list into its head and tail
  let numbers = [1, 2, 3]
  let summary = case numbers {
    [] -> "empty"
    [single] -> "one item: " <> int.to_string(single)
    [first, ..rest] ->
      "starts with "
      <> int.to_string(first)
      <> ", "
      <> int.to_string(length(rest))
      <> " more"
  }
  io.println(summary)
}

fn length(items: List(a)) -> Int {
  case items {
    [] -> 0
    [_, ..rest] -> 1 + length(rest)
  }
}

The tuple #(0, 5) matches #(0, _) because its first element is 0 and the second is anything, so the result is “on the y-axis”. The list [1, 2, 3] is neither empty nor a single item, so it matches [first, ..rest], binding first to 1 and rest to [2, 3]. This head-and-tail destructuring is the foundation of list processing in Gleam.

Recursion: Repetition Without Loops

Gleam has no for or while loops. Repetition is expressed with recursion — a function that calls itself, using case to decide when to stop. Pairing recursion with an accumulator argument gives tail recursion, which the BEAM runs in constant stack space.

Create a file named 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import gleam/io
import gleam/int

pub fn main() {
  // Recursion replaces the imperative `for`/`while` loop
  io.println("Countdown:")
  countdown(5)
  io.println("Liftoff!")

  // A classic recursive factorial
  io.println("5! = " <> int.to_string(factorial(5)))

  // Tail recursion with an accumulator sums 1..100 efficiently
  io.println("Sum 1..100 = " <> int.to_string(sum_to(100, 0)))
}

fn countdown(n: Int) -> Nil {
  case n {
    0 -> Nil
    _ -> {
      io.println(int.to_string(n))
      countdown(n - 1)
    }
  }
}

fn factorial(n: Int) -> Int {
  case n {
    0 -> 1
    _ -> n * factorial(n - 1)
  }
}

fn sum_to(n: Int, acc: Int) -> Int {
  case n {
    0 -> acc
    _ -> sum_to(n - 1, acc + n)
  }
}

Each function uses case to test for its base case (0) and otherwise recurses on a smaller value. The sum_to function carries the running total in its acc parameter and only calls itself in tail position, so it never grows the call stack — the idiomatic way to loop a fixed number of times in Gleam.

Iterating Over Collections

For working with lists, you rarely write the recursion by hand. The standard library’s list module provides higher-order functions that express the common patterns — running a side effect, transforming, filtering, and reducing — without an explicit loop.

Create a file named list_iteration.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/io
import gleam/int
import gleam/list
import gleam/string

pub fn main() {
  let numbers = [1, 2, 3, 4, 5]

  // list.each runs a function on each element for its side effect
  io.println("Each number:")
  list.each(numbers, fn(n) { io.println("  " <> int.to_string(n)) })

  // list.map transforms every element, returning a new list
  let doubled = list.map(numbers, fn(n) { n * 2 })
  io.println("Doubled: " <> string.inspect(doubled))

  // list.filter keeps only the elements that satisfy a predicate
  let evens = list.filter(numbers, fn(n) { n % 2 == 0 })
  io.println("Evens: " <> string.inspect(evens))

  // list.fold reduces a list to a single value
  let total = list.fold(numbers, 0, fn(acc, n) { acc + n })
  io.println("Sum: " <> int.to_string(total))
}

These functions are where a Gleam programmer’s mind goes first: instead of “loop over the list and accumulate a sum,” you write list.fold. Because the list is immutable, list.map and list.filter return brand-new lists and leave the original numbers untouched. The string.inspect function renders any value as a readable string, which is handy for printing lists.

Running with Docker

Gleam needs a project structure, so each Docker command creates one on the fly and copies your source file into it as hello.gleam before running.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Pull the official Gleam image
docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine

# Run the conditionals example
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/conditionals.gleam hello/src/hello.gleam && cd hello && gleam run'

# Run the pattern matching example
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/pattern_matching.gleam hello/src/hello.gleam && cd hello && gleam run'

# Run the recursion example
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/recursion.gleam hello/src/hello.gleam && cd hello && gleam run'

# Run the list iteration example
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/list_iteration.gleam hello/src/hello.gleam && cd hello && gleam run'

Expected Output

Running conditionals.gleam produces:

18 degrees is comfortable
-7 is negative
Age 20: adult

Running pattern_matching.gleam produces:

Sat is a weekend
on the y-axis
starts with 1, 2 more

Running recursion.gleam produces:

Countdown:
5
4
3
2
1
Liftoff!
5! = 120
Sum 1..100 = 5050

Running list_iteration.gleam produces:

Each number:
  1
  2
  3
  4
  5
Doubled: [2, 4, 6, 8, 10]
Evens: [2, 4]
Sum: 15

Key Concepts

  • No if statement — Gleam has no if, switch, for, or while. The case expression is the single branching construct, and recursion or list functions handle repetition.
  • case is an expression — It evaluates to a value you can bind with let or pass along, which is why Gleam needs no separate ternary operator.
  • Guards refine patterns — Writing if after a pattern (t if t < 15 ->) lets a branch match only when an extra condition holds; patterns are tried top to bottom.
  • Exhaustiveness checking — The compiler rejects a case that doesn’t cover every possibility, catching missed cases before the program runs.
  • Destructuring while matching — Patterns can pull apart tuples (#(x, y)) and lists ([first, ..rest]), and | lets one branch match several alternatives.
  • Recursion replaces loops — A function calls itself with a smaller value and uses case for its base case; an accumulator argument gives tail recursion that runs in constant stack space.
  • List functions over manual loopslist.each, list.map, list.filter, and list.fold express iteration declaratively and return new lists, since all data is immutable.
  • No truthy valuescase on a Bool matches True/False explicitly; Gleam never treats numbers, strings, or lists as conditions.

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