Functions, Pattern Matching, Lists, and Pipelines
- Gleam has no
fororwhileloops: use recursion instead. - Tail recursion with an accumulator prevents stack overflow for large inputs.
- The
|>pipeline operator threads a value through a sequence of transformations.
Defining Functions
- A Gleam function is introduced with
fn, followed by a name, parameter list, optional return type, and a body
fn add(a: Int, b: Int) -> Int {
a + b
}
- Parameter types are written
name: Type - The return type after
->is optional but recommended - The body is a block of expressions, the last of which is the function's result
pub fnexports the function; plainfnkeeps it private to the module- Functions can call themselves recursively and can be passed as values
Pattern Matching
- Gleam's
caseexpression is the primary tool for working with structured data - It can match on custom types, lists, tuples, integers, and strings
- And can match several values simultaneously
type Triangle {
Equilateral
Isosceles
Scalene
}
fn classify_triangle(a: Int, b: Int, c: Int) -> Triangle {
case a, b, c {
x, y, z if x == y && y == z -> Equilateral
x, y, _ if x == y -> Isosceles
_, y, z if y == z -> Isosceles
x, _, z if x == z -> Isosceles
_, _, _ -> Scalene
}
}
type Triangle { Equilateral Isosceles Scalene }defines the three possible resultscase a, b, c { ... }matches on three values at once- This is more concise than three nested
ifstatements
- This is more concise than three nested
x, y, z if x == y && y == z -> Equilateralis a guarded arm- The pattern
x, y, zbinds the three values to names - The
ifcondition must also be true for the arm to match - Without guards this would require a
caseinside acase
- The pattern
- The arms are tried in order, with the first match winning
- The compiler verifies that the arms cover all possibilities
fn is_palindrome(s: String) -> Bool {
let chars = string.to_graphemes(s)
chars == list.reverse(chars)
}
string.to_graphemessplits a string into a list of characters- Graphemes, not bytes, so emoji and accented letters work
list.reversereturns a new list without modifying the original==compares lists element by element
Recursion Instead of Loops
- Gleam has no
fororwhileloops - Repetition is expressed through recursion
fn length(lst: List(a)) -> Int {
case lst {
[] -> 0
[_, ..rest] -> 1 + length(rest)
}
}
case lst { [] -> 0 [_, ..rest] -> 1 + length(rest) }is the standard recursive structure on lists[]matches the empty list, so the base case returns 0[_, ..rest]matches a non-empty list:_discards the head,restbinds the tail
- The type parameter
ameanslengthworks on any list regardless of the elements' type - This version grows the call stack with each recursive call
- That's a problem for large lists
Tail Recursion
- Tail recursion eliminates stack growth by accumulating the result in a parameter instead of on the call stack
fn factorial(n: Int) -> Int {
factorial_tail(n, 1)
}
fn factorial_tail(n: Int, acc: Int) -> Int {
case n {
0 -> acc
_ -> factorial_tail(n - 1, acc * n)
}
}
factorialis the public entry point: it calls the worker with an initial accumulator of 1factorial_tailis the tail-recursive worker- When
nis 0, return the accumulated result - Otherwise, multiply
accbynand recurse withn - 1 - The recursive call is the last operation; nothing waits for it to return
- When
- The Gleam compiler (and the BEAM VM) optimise tail calls so they do not grow the stack
fn take(lst: List(a), n: Int) -> List(a) {
case lst, n {
_, 0 -> []
[], _ -> []
[x, ..rest], _ -> [x, ..take(rest, n - 1)]
}
}
length nums = 5
length [] = 0
factorial(5) = 120
factorial(0) = 1
take 3 = [1, 2, 3]
take 10 = [1, 2, 3, 4, 5]
- Matching on two values simultaneously:
case lst, n { ... } - Three base cases:
n = 0(done), empty list (done), otherwise take one element and recurse - The result is built by prepending
[x, ..take(rest, n - 1)]
Lists and Higher-Order Functions
- Gleam's
Listtype is a singly-linked list - The standard library provides
list.map,list.filter, andlist.foldthat cover most iteration needs list.map(lst, f)appliesfto every element and returns a new list- Like Python's
[f(x) for x in lst]
- Like Python's
list.filter(lst, pred)keeps elements wherepredreturnsTrue- Like Python's
[x for x in lst if pred(x)]
- Like Python's
list.fold(lst, init, f)combines elements left to right usingflist.fold([1,2,3], 0, fn(acc, x) { acc + x })gives6- Like Python's
functools.reduce(f, lst, init)
- Anonymous functions use
fn(args) { body }:
list.map([1, 2, 3], fn(x) { x * 2 })
// => [2, 4, 6]
The Pipeline Operator
- The
|>operator passes the result of one expression as the first argument of the next - Lets you write a sequence of transformations left to right
- Easier to read than nested calls
let result =
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|> list.map(fn(x) { x * 2 })
|> list.filter(fn(x) { x > 10 })
|> list.fold(0, fn(acc, x) { acc + x })
io.println("pipeline result is " <> string.inspect(result))
pipeline result is 80
[1, 2, 3, 4, 5] |> list.map(fn(x) { x * 2 })doubles every element|>passes the list as the first argument tolist.mapfn(x) { x * 2 }is an anonymous function
|> list.filter(fn(x) { x > 10 })keeps only values greater than 10|> list.fold(0, fn(acc, x) { acc + x })sums the remaining values- The whole pipeline reads left to right: double, filter, sum
- Much cleaner than Python equivalent
reduce(lambda a, x: a+x, filter(lambda x: x>10, map(lambda x: x*2, lst)), 0)
Check Understanding
What does "exhaustive" mean for a case expression?
An exhaustive match covers every possible value of the type being matched.
If you write a case on a Triangle but only handle Equilateral and
Isosceles, the compiler reports an error because Scalene is not
covered.
This prevents a common class of bug where a new variant is added to a type
and existing match expressions silently fall through to a catch-all.
When should you use list.fold instead of list.map or list.filter?
Use list.fold when you need to combine all elements of a list into a single value,
like a sum or a product.
list.map transforms each element independently;
list.filter selects a subset of elements.
Many operations that would use a for loop in Python can be expressed
with one of these three functions in Gleam.
Exercises
Recursive sum (5 minutes)
Write sum(lst: List(Int)) -> Int using tail recursion with an accumulator.
Verify that sum([1, 2, 3, 4, 5]) returns 15 and sum([]) returns 0.
Palindrome checker (10 minutes)
Write is_palindrome(s: String) -> Bool using string.to_graphemes and list.reverse.
Test it with "racecar", "gleam", and at least one string with a non-ASCII character.
Pipeline challenge (10 minutes)
Use a single pipeline to take the list [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
keep only odd numbers, square each one, and sum the results.
The answer should be 165.
Triangle classifier (10 minutes)
Extend the classify_triangle function to also return a label for right triangles.
A triangle with sides a, b, c (where c is the longest side)
is a right triangle if a*a + b*b == c*c.
Determine the correct arm ordering so that an equilateral triangle is never labelled as right.