Gleam touts itself as a language with minimal syntax, and it is true that it wouldn't take you much longer than a day to understand all of its syntax. In the spirit of only having one way of doing things, Gleam doesn't have either loops, nor if statements.
Instead, Gleam wants you to make extensive use of pattern matching and recursion. Here is the example of how you would compute the sum of the elements of a list in Gleam:
pub fn sum_list(lst: List(Int), acc: Int) -> Int {
case lst {
[] -> acc
[first, ..rest] -> sum_list(rest, acc + first)
}
}or using a higher order function:
import gleam/list
pub fn sum_list(lst: List(Int)) -> Int {
list.fold(lst, 0, fn(item, acc) { item + acc })
}But there is one feature that eluded me for quite a while: the use keyword.
What is it?
Because Gleam is a functional programming language, it largely revolves around passing around functions. When doing that, the flow of function is done via "callback functions".
Say we want to parse a string of type key=value;... with only three valid keys: name, email and age. A naive way to extract these values from that string would look like this:
import gleam/int
import gleam/list
import gleam/string
pub type UserInfo {
UserInfo(name: String, email: String, age: Int)
}
pub fn parse(s: String) -> Result(UserInfo, String) {
// First split the string by semicolons, and then only keep
// the sub-strings that can be split by the equal sign
let vals =
string.split(s, ";")
|> list.filter_map(string.split_once(_, "="))
// Parsing of the sub-strings
case list.key_find(vals, "name") {
Error(_) -> Error("Missing name")
Ok(name) -> {
case list.key_find(vals, "email") {
Error(_) -> Error("Missing email")
Ok(email) -> {
case list.key_find(vals, "age") {
Error(_) -> Error("Missing age")
Ok(age_str) -> {
case int.parse(age_str) {
Ok(age) -> Ok(UserInfo(name:, email:, age:))
_ -> Error("Couldn't parse age")
}
}
}
}
}
}
}
}Ew, right? All of this indentation leaves a lot to be desired. We can at the very least try to refactor this into functions to try to clean it up a bit.
import gleam/int
import gleam/list
import gleam/string
pub type UserInfo {
UserInfo(name: String, email: String, age: Int)
}
/// Parse a single key and keep going
fn parse_single(
lst: List(#(String, String)),
key: String,
next: fn(List(#(String, String)), String) -> Result(a, String),
) -> Result(a, String) {
case list.key_pop(lst, key) {
Error(_) -> Error("Missing " <> key)
Ok(#(v, lst_new)) -> next(lst_new, v)
}
}
/// Parse an integer from a string
fn parse_int(v: String, next: fn(Int) -> Result(c, String)) {
case int.parse(v) {
Error(_) -> Error("Invalid age")
Ok(value) -> next(value)
}
}
/// Our "improved" parsing function
pub fn parse_2(s: String) -> Result(UserInfo, String) {
let vals =
string.split(s, ";")
|> list.filter_map(string.split_once(_, "="))
parse_single(vals, "name", fn(vals, name) {
parse_single(vals, "email", fn(vals, email) {
parse_single(vals, "age", fn(_vals, age_str) {
parse_int(age_str, fn(age) { Ok(UserInfo(name:, email:, age:)) })
})
})
})
}This new approach uses callback functions to keep going after each value has been parsed. If at any point, an error was found, the callback function is not called, and the parent function will return the error.
Although the indentation is slightly better than our naive implementation, it is easy to see how this could blow out of proportion if the number of values to parse were to grow larger.
✨ Thankfully, the
usekeyword is here to save us! ✨
use is the closest thing Gleam has to "magic", but remains relatively simple in its usefulness.
Simply put, the two pieces of code below are completely equivalent in Gleam:
fn my_function(v, callback) {
todo
}
// Without use
my_function(55, fn(v) { v * 2 })
// With use
use v <- my_function(55)
v * 2But the second one reads much better, especially when the number of function calls increases:
// Without use
my_function(55, fn(v) {
my_function(v * 2, fn(v) { my_function(v + 3, fn(v) { v - 5 }) })
})
// With use
use v <- my_function(55)
use v <- my_function(v * 2)
use v <- my_function(v + 3)
v - 5Use cases
Early return
Gleam uses implicit returns and as such, the language does not allow us to return early from functions.
However, the standard library provides a function called bool.guard, which is very simple in its implementation:
// bool.gleam
pub fn guard(
when requirement: Bool,
return consequence: t,
otherwise alternative: fn() -> t,
) -> t {
case requirement {
True -> consequence
False -> alternative()
}
}To simulate an early return, we
import gleam/bool
pub fn func_with_early_return(v: Int) {
use v <- bool.guard(v > 5, 0)
v - 5
}