Using use
in Gleam
Recently, a colleague checked out Gleam’s language tour. They liked what they saw, but they were confused by Gleam’s use
syntax. I like Gleam’s syntax a lotI even wrote an article about Gleam’s syntax., but I was also confused by use
when I first encountered it. Here’s how I use use
:The use
expression was introduced in Gleam v0.25, as a more general replacement for the try
keyword that preceded it.
What is use
anyway?
use
is a Gleam expression that allows me to write code that uses a callbackI’ve heard debate over whether a function argument to a higher order function is always a callback, but the language tour for Gleam uses “callback function”, so I’ve used callback here. function in an unindented style. Specifically, for functions whose last argument is a callback function. For example:
import gleam/list
fn catify_without_use(strings: List(String)) -> List(String) {
list.map(strings, fn(string) {
string <> " cat"
})
}
Here, I take a list of strings and append " cat"
to the end of each of them. list.map
takes a list as the first argument, and a callback function that takes a list element and returns a new list element. Here’s what that same function looks like with a use
expression:
import gleam/list
fn catify_with_use(strings: List(String)) -> List(String) {
use string <- list.map(strings)
string <> " cat"
}
Here the use
expression has done three things:
First, we’ve moved the arguments for the callback function to the left of the arrow. fn(string)
becomes use string <-
.The name string
is an identifier chosen by me, it could just as easily be use to_be_catted <- ...
Second, we’ve changed list.map
into a function that takes one less argument than it usually does. It’s now list.map(strings)
as though it only took one argument. list.map
still takes two arguments, but use
has changed how we write that second argument.This aspect of use
makes it easier to see what the non callback arguments of a use
d function are, since the callback is often the bulkiest part of the outer function call signature.The use
statement has exactly one identifier, because that’s how many arguments list.map
’s callback function needs, see this note for more examples.
Third, the body of the callback function is now below the line of the use
expression, indented in line with the use
expression. Everything below the use
expression until the end of catify_with_use
becomes the body for the callback function.That is, everything in the same block as the use
expression. See this note for more explanation.
So What?
I’ve introduced some syntax sugarA use
expression is syntactic sugar for a regular call and an anonymous function. During compilation, a use
expression expands into the fn(arg1, arg2, ...) { body() }
form. It’s explicitly to allow a different way of writing expressions. that allows me to change how I write functions that take a callback function as their last argument. But in my example, it doesn’t help me much. In the example, it’s no longer clear that I’m writing a callback function, and I can’t do anything else after my list.map
call, that doesn’t end up in the callback function. list.map
can be used with a use
expression, but it’s a poor choice over the default syntax in most cases.I chose list.map
because it’s a commonly used Gleam function that is a poor fit for use
. list.map
is one of my favorite functions though, and I’ve used it a lot in one of my other Gleam articles.
So what is use
useful for?
result.unwrap
and early returns
Gleam has no exceptions, all errors must be returned as values from a function.Gleam also has no return
keyword, the last expression in a block is returned as the value of that block. In a function, that’s the last thing in the function. Specifically, Gleam uses the convention of Result
to capture this information. Success looks like Ok(value)
and failure looks like Error(reason)
.
But what if I want to do something that might fail, and then continue to do something else in the same function.
import gleam/result
fn outer() -> Result(success, failure) {
let id = parse_id() |> result.unwrap(0)
... // More code that uses the id
}
parse_id
might fail, and therefore returns a Result
. To use the inner value, we need to unwrap it somehow. On a successful parse, the wrapped value will look like Ok(id)
, and this code uses result.unwrap
to pull out the id
on an Ok
case, or set the id
to 0 in the Error
case.
But 0
as an id is made up, and likely has no reliable meaning in our system. If we wanted to convey that the id failed to parse, we already had an Error
that we could have returned directly.
Instead of using result.unwrap
, we can use result.map
Both list.map
and result.map
are named map
, because they are examples of the higher order function map that applies a function to every element of collection.In the case of result.map
the “collection” is only the contents of the Ok
, Error
s are simply returned without invoking the callback function., which takes a Result
and a callback function, where the callback function is only invoked when the Result
is Ok
. If the Result is an Error
then it returns the Error
.This is how Gleam can do an “early” return, result.map
only evaluates the callback function on success, so in the failure case the value of the result.map
expression (and therefore the block) is the Error
, and it doesn’t evaluate any more code. The callback function gets the unwrapped, inner value as its one argument.
So we can do:
import gleam/result
fn outer() -> Result(success, failure) {
result.map(parse_id(), fn(id) {
... // More code that uses the id
})
}
The problem with invoking result.map
this way, is that now all of the internals of outer
are indented inside the callback function. Here, we can use a use
expression to eliminate the extra indentation and focus our function on the success case:
import gleam/result
fn outer() -> Result(success, failure) {
use id <- result.map(parse_id())
... // More code that uses the id
}
We have still accomplished what we wanted to accomplish, which is that if parse_id
fails, it returns early with the Error
.If you’re familiar with Rust’s ?
operator, the expression use id <- result.map(parse_id())
is equivalent to Rust’s let id = parse_id()?;
Now, we can focus our codeand limited attention on the unwrapped id
in the success case.
Avoiding boilerplate with result.map
A use
expression can also allow you to avoid a lot of boilerplate. For example, reading from a file is an operation that can fail, so it returns a Result
. If I want to read lines from a file and then transform them, I can use result.map
:
import gleam/result
import gleam/list
fn transform_lines() {
read_file_lines()
|> result.map(list.filter(...))
|> result.map(list.map(...))
|> result.map(list.sort(...))
|> result.map(something_else())
}
but because result.map
returns a new Result
, I have to continue chaining result.map
calls until I’ve finished all of the transformations that I need.
A use
expression allows us to gracefully handle the failure case, while removing the need for chained calls to result.map
:
import gleam/result
import gleam/list
fn transform_lines() {
use lines <- result.map(read_file_lines())
lines
|> list.filter(...)
|> list.map(...)
|> list.sort(...)
|> something_else()
}
These two functions are equivalent, but the use
expression allows us to focus on the transformations we care about.Since use
is only syntax sugar, all of the piped transformations could be written inside result.map
’s callback function without a use
expression:fn(lines) { ... }
.
Chaining result.try
use
expressions are especially helpful when doing multiple different things that might fail. result.try
takes a Result
and callback function that itself returns a Result
.try
the first thing, then try
a second thing if the first one succeeds.I’ve heard this concept of chaining result.try
calls together referred to as railroad-oriented design. If any operation fails, it switches to the Error
“track” and returns that Error
, otherwise it continues along the Ok
track until the next operation that could fail, which has another “railroad switch”. If the first argument is an Error
it returns that error. Otherwise, it evaluates the callback function and returns whatever Result
that callback function does. For example:
import gleam/result
fn handle_form(form_data: RegistrationForm) {
result.try(
unique_username(form_data.username),
fn(username) {
result.try(
validate_password(form_data.password),
fn(password) {
result.map(
register_user(username, password),
fn(user) {
"welcome " <> user.username <> "!"
}
)
}
)
}
)
}
Because each operation can separately fail, we can’t chain these together like I did in the result.map
boilerplate example. This creates a cascade of indented callback functions that makes it hard to keep track of what’s going on and what the final return value in the success case is.I’ve heard this referred to as callback hell. With use
expressions the meaning is much clearer:
import gleam/result
fn handle_form(form_data: RegistrationForm) {
use username <- result.try(unique_username(form_data.username))
use password <- result.try(validate_password(form_data.password))
use user <- result.map(register_user(username, password))
"welcome " <> user.username <> "!"
}
Here it’s much easier to tell which operations we’re doing, and what the final return value is.The last operation uses result.map
because we’re finally returning something that can’t fail. It was the same in the first example, did you notice?
Context Management
Another place where use
expressions shine is with context management: functions that do setup, cleanup, or both. In Gleam, there’s no special way of handling context management, and so these functions use a callback function as an argument to wrap the user behavior they’re managing. For example, opening a database connection with sqlight
:
import sqlight
fn get_data() {
use conn <- sqlight.with_connection("my_database.db")
... // query the database
}
sqlight.with_connection
will open a connection to the database, execute code, and then close the connection afterwards. The database connection is available as conn
for the rest of the function. Technically, all of the user code is wrapped in a function:
import sqlight
fn get_data() {
sqlight.with_connection("my_database.db", fn(conn) {
... // query the database
})
}
but the use
expression allows us to focus on the query we want to write, not on database management.
Another example of this kind of callback function wrapper comes from wisp
, Gleam’s web framework:This example comes from wisp’s logging example.
import wisp
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
let req = wisp.method_override(req)
use <- wisp.log_request(req)
use <- wisp.rescue_crashes
use req <- wisp.handle_head(req)
handle_request(req)
}
Here wisp.log_request
uses a callback function to allow logging to take place after a request has been handled, regardless of how the user of wisp
chooses to do that handling.
The other wisp
functions use a similar pattern allowing for customization to the application while still handling core web application concerns.
This is also an example of use
expressions preventing cascading nested callbacks, without use
:
import wisp
pub fn middleware(
req: wisp.Request,
handle_request: fn(wisp.Request) -> wisp.Response,
) -> wisp.Response {
let req = wisp.method_override(req)
wisp.log_request(req, fn() {
wisp.rescue_crashes(fn() {
wisp.handle_head(req, fn(req) {
handle_request(req)
})
})
})
}
Without use
, how the request is being handled is obscured by the nested callback functions used to manage wisp
request context.This example has three of these context manager functions, but I’ve seen wisp
applications with 8 or more. You could write that without use
, but the use
expressions allow the custom logic to remain readable.
Takeaways
I’ve shared a number of examples where use
expressions can add clarity to code, both with error handling and context management. As I showed in the list.map
example, use
expressions aren’t always helpful. The key is to use use
when it allows you to highlight the happy path of your code, while handling concerns like failure and logging.Thank you to Nicole, Jeff Miller, and Mark, as well as the Gleam discord for helping me write and edit this article.
A use
expression is syntax sugar, and it’s always possible to write Gleam code without it, though maybe not as clearly.Why use
, specifically, as a keyword? This issue highlighted the need for a general syntax sugar. with
statements in koka were identified as a similar solution, but with
was already a well-used special form in Elixir, and Gleam didn’t want to confuse BEAM programmers coming from Elixir.After a lot of discussion use
was chosen, in part because it’s the same length as let
and it wasn’t already in use anywhere.
Additional Notes
A note of use
scope
Everything below the use
expression comprises the body of the callback function until the end of the current block. By default, this will be the end of the function, but we can use {}
to create a smaller block:From the language tour: “use
is an expression like everything else in Gleam, so it can be placed within blocks.”
import gleam/result
fn example() {
let smaller_block = {
use value <- result.try(thing_that_might_fail())
... // do something with the value
}
no_longer_in_use_callback(smaller_block)
}
A example of this comes from the new decode
library:This example comes from the README for decode
.
let decoder =
decode.into({
use name <- decode.parameter
use email <- decode.parameter
use is_admin <- decode.parameter
User(name, email, is_admin)
})
|> decode.field("name", decode.string)
|> decode.field("email", decode.string)
|> decode.field("is-admin", decode.bool)
decoder
|> decode.from(data)
Here, decode.into
is using a {}
block to succinctly create a decoder function using a combination of use
and decode.parameter
.This is a clever use of use
as syntax sugar, since decode.parameter
simply returns its argument. So the above block translates to:fn(name) { fn(email) { fn(is_admin) { User(name, email, is_admin) }}}
Which is much harder to read, especially since the ordering of fields matters.
A note on use
arguments
The number of arguments in the use
expression are exactly the same as the arguments required for the callback function being replaced.
bool.guard
bool.guard
takes a function that requires no arguments:fn() -> a
// without `use`
bool.guard(condition, "early return", fn() {
...
"late return"
})
// with `use`
use <- bool.guard(condition, "early return")
...
"late return"
list.fold
list.fold
takes a functions that requires two arguments.fn(a, a) -> a
// without `use`
list.fold(numbers, 1, fn(accumulator, element) {
accumulator * element
})
// with `use`
use accumulator, element <- list.fold(numbers, 1)
accumulator * element