If you’ve used let! in an F# computation expression, Gleam’s use may look like familiar binding syntax. Both can flatten code that would otherwise nest. They aren’t the same mechanism.
Table of contents
On this page
Start with Ordinary Result Binding
Let’s parse two positive dimensions and calculate their area. F# has Result.bind, but doesn’t ship a built-in result { ... } computation-expression builder. We can express this without adding one:
let positiveDimension (text: string) =
match System.Int32.TryParse text with
| true, value when value > 0 -> Ok value
| _ -> Error "Expected a positive integer"
let area widthText heightText =
positiveDimension widthText
|> Result.bind (fun width ->
positiveDimension heightText
|> Result.bind (fun height -> Ok (width * height)))scratch.fsx
The direct Gleam translation uses result.try with ordinary callbacks:
import gleam/int
import gleam/list
import gleam/result
fn positive_dimension(text: String) -> Result(Int, String) {
case int.parse(text) {
Ok(value) if value > 0 -> Ok(value)
_ -> Error("Expected a positive integer")
}
}
fn area_nested(width_text: String, height_text: String) -> Result(Int, String) {
result.try(positive_dimension(width_text), fn(width) {
result.try(positive_dimension(height_text), fn(height) {
Ok(width * height)
})
})
}src/learning_gleam.gleam
result.try calls its callback only for Ok. For Error, it returns the error without running the callback. That explains the short-circuiting behaviour without needing any special return statement.
Flatten the Callbacks with use
Here’s the same function with less indentation:
fn area(width_text: String, height_text: String) -> Result(Int, String) {
use width <- result.try(positive_dimension(width_text))
use height <- result.try(positive_dimension(height_text))
Ok(width * height)
}src/learning_gleam.gleam
The 1st use inserts a callback as the final argument to result.try. Its parameter is width. Everything after that line becomes the callback body, including the 2nd use.
It’s not an unconditional early return or a new error-handling primitive. It’s the same function call as the nested version. The errors must still have compatible types, so map them into a shared error type before chaining when necessary.
It’s Not Just for Results
Any suitable callback-taking function can work with use. For example, list.map calls its callback once per element:
fn greetings(names: List(String)) -> List(String) {
use name <- list.map(names)
"Hello, " <> name
}src/learning_gleam.gleam
Here the rest of the block runs twice for two names, rather than once for a successful result. A different callback API might defer execution. Read the receiving function’s contract instead of assuming that use means asynchronous work or error propagation.
Also, Gleam’s use isn’t F#‘s resource-disposal use. It doesn’t automatically provide Dispose, finally, or cleanup. Any such behaviour must come from the function receiving the callback.
Try It
Combine the Gleam blocks in src/learning_gleam.gleam and run gleam run:
pub fn main() -> Nil {
assert area("3", "4") == Ok(12)
assert area("bad", "4") == Error("Expected a positive integer")
assert area("3", "0") == Error("Expected a positive integer")
assert area("3", "4") == area_nested("3", "4")
assert area("bad", "4") == area_nested("bad", "4")
assert area("3", "0") == area_nested("3", "0")
assert greetings(["Ada", "Grace"]) == ["Hello, Ada", "Hello, Grace"]
assert greetings([]) == []
}src/learning_gleam.gleam
For F#:
assert (area "3" "4" = Ok 12)
assert (area "bad" "4" = Error "Expected a positive integer")
assert (area "3" "0" = Error "Expected a positive integer")scratch.fsx
Use the flattened version when it makes a chain easier to follow. For a short callback, an ordinary function call is often clearer.
Further reading: use expressions and result.try.