Hi, I’m Erika Rowland (a.k.a. erikareads). Hi, I’m Erika. I’m an Ops-shaped Software Engineer, Toolmaker, and Resilience Engineering fan. I like Elixir and Gleam, Reading, and Design. She/Her. Constellation Webring Published on Modified on

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 used 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.mapBoth 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, Errors 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

Constellation Webring