How to Return a Default List in a Gleam Error Case Clause
Understanding Expression Blocks in Gleam
When transitioning to Gleam from languages like Elixir, Rust, or JavaScript, it is common to run into syntax hurdles when trying to run side effects (like logging) before returning a fallback value. If you try to sequence multiple expressions inside a case clause without grouping them, Gleam's compiler will throw an error because each case arm expects exactly one expression.
Why the Original Code Fails
In Gleam, a case clause follows the pattern Pattern -> Expression. When you write:
Error(e) ->
e
|> error_label
|> io.println_error
[]
The compiler does not treat these as two sequential statements. Instead, it expects a single expression right after the arrow ->.
Furthermore, attempting to pipe the result of io.println_error into list.new fails because io.println_error returns Nil. The pipe operator (|>) injects that Nil as the first argument, but list.new takes zero arguments, resulting in an arity mismatch.
The Solution: Wrap Multiple Expressions in a Block
To execute a side effect (such as printing an error to standard error) and then return an empty list [], wrap the body of your case arm in curly braces { ... }. A block evaluates all expressions in sequence and evaluates to the value of the final expression:
import gleam/io
import gleam/list
import simplifile
fn list_files(dir: String) -> List(String) {
case simplifile.read_directory(at: dir) {
Ok(entries) ->
entries
|> list.filter(fn(name: String) {
case simplifile.is_file(dir <> name) {
Ok(True) -> True
_ -> False
}
})
Error(e) -> {
e
|> error_label
|> io.println_error
// This final expression defines the return value of the block
[]
}
}
}
Idiomatic Alternatives in Gleam
While wrapping expressions in { ... } solves the problem directly, Gleam offers several functional patterns that can make this logic even cleaner:
1. Use result.unwrap or result.map
If you separate logging from value extraction, you can leverage the standard gleam/result module:
import gleam/result
fn list_files(dir: String) -> List(String) {
simplifile.read_directory(at: dir)
|> result.map(fn(entries) {
list.filter(entries, fn(name) {
simplifile.is_file(dir <> name) == Ok(True)
})
})
|> result.lazy_unwrap(fn() {
io.println_error("Failed to read directory")
[]
})
}
2. Safer Path Concatenation
As an added tip, using string concatenation (dir <> name) can easily cause path bugs if dir does not end with a trailing slash. Consider using filepath.join(dir, name) from the filepath package for robust cross-platform path handling.
Summary
- Each
casearm in Gleam expects a single expression. - To run multiple expressions (such as logging an error and returning a default value), group them inside a block
{ ... }. - The value returned by the block will be the value of its last expression.