v0.25 - Introducing use expressions!

Gleam is a type safe and scalable language for the Erlang virtual machine and JavaScript runtimes. Today Gleam v0.25.0 has been released, featuring a long-awaited new feature: use expressions.

The motivation

Two of Gleam’s main goals are to be easy to learn and easy to work with. In aid of these goals Gleam the language is designed to be as small and consistent as possible, drawing inspiration from other languages such as Elm, Go, and Lua.

Because of this Gleam lacks exceptions, macros, type classes, early returns, and a variety of other features, instead going all-in with just first-class-functions and pattern matching. All flow control in Gleam programs is explicit and all functionality is built from data being passed to and returned from functions.

This small set of tools works well for making Gleam code approachable but has one irritation: indentation. Certain patterns in Gleam result in code with more indentation than you might get in other languages.

pub fn login(credentials) {
  case authenticate(credentials) {
    Error(e) -> Error(e)
    Ok(user) ->
      case fetch_profile(user) {
        Error(e) -> Error(e)
        Ok(profile) -> render_welcome(user, profile)
      }
  }
}

To remedy this common problem we introduced the try expression in v0.9, which made it possible to write code that used the Result type (Gleam’s way of representing a possible failure) without any indentation. The code above could be written like so.

pub fn login(credentials) {
  try user = authenticate(credentials)
  try profile = fetch_profile(user)
  render_welcome(user, profile)
}

This has worked well for us so far as most of the time these patterns of nesting come from code that works with the Result type, however as more applications are written in Gleam it has become clear that we want to avoid indentation in other situations as well.

As a concrete example, here is a HTTP request handler written in Python.

def handle_request(request: HttpRequest) -> HttpResponse:
    with (
        logger.span("handle_request"),
        database.connection() as conn,
    ):
        if request.method != "POST":
            return method_not_allowed_response()

        try:
            record = database.insert(conn, request.body)
        except InsertError as exc:
            return bad_request_response(exc)
          
        return created_response(record)

If this code was replicated in Gleam it might look like this:

pub fn handle_request(request: HttpRequest) {
  logger.span("handle_request", fn() {
    database.connection(fn(conn) {
      case request.method {
        Post ->
          case database.insert(conn, request.body) {
            Ok(record) -> created_response(record)
            Error(exc) -> bad_request_response(exc)
          }
        _ -> method_not_allowed_response()
      }
    })
  })
}

It is unsatisfying that this code isn’t easier to read and more aesthetically pleasing. Web backends are a good use case for Gleam, and there are plenty of other domains and types of application that will have similar issue.

After much discussion and research the solution we have come up with is the new use expression. We believe it manages to be expressive and capable enough to resolve all these problems while being conceptually simple compared to most other solutions.

Here’s what the same Gleam code might look like with use:

pub fn handle_request(request: HttpRequest) {
  use <- logger.span("handle_request")
  use <- require_method(request, Post)
  use conn <- database.connection()

  case database.insert(conn, request.body) {
    Ok(record) -> created_response(record)
    Error(exc) -> bad_request_response(exc)
  }
}

The trade off here is that it is less immediately obvious what use does to a newcomer. We have introduced some additional complexity to the language, but we think this additional learning requirement is a worthwhile trade for the better developer experience, and Gleam is still a small language compared to most.

What does use do?

The use expression is a bit of syntactic sugar that turns all following expressions into an anonymous function that gets passed as an additional argument to a function call.

For example, imagine I had a function called with_file which would open a file, pass the open file to a given function so it can read from or write to it, and then closes the file afterwards.

// Define the function
pub fn with_file(path, handler) {
  let file = open(path)
  handler(file)
  close(file)
}

// Use it
pub fn main() {
  with_file("pokemon.txt", fn(file) {
    write(file, "Oddish\n")
    write(file, "Farfetch'd\n")
  })
}

With use this function could be called without extra indentation. This example below with use compiles to exactly the same code as the one above.

pub fn main() {
  use file <- with_file("pokemon.txt")
  write(file, "Oddish\n")
  write(file, "Farfetch'd\n")
}

And it’s not just limited to a single argument, functions of any arity can be used, including functions that accept no arguments at all.

What can it be used for?

The use expression is highly generic and isn’t restricted to any particular types, interfaces, or laws, so it can applied to many different things.

It could be used to create HTTP middleware, as seen in the web service example above.

pub fn require_method(request, method, continue) {
  case request.method == method {
    True -> continue()
    False -> method_not_allowed()
  }
}

pub fn handle_request(request) {
  use <- require_method(request, Post)
  // ...
}

It could be used to replicate Go’s defer statements.

pub fn defer(cleanup, body) {
  body()
  cleanup()
}

pub fn main() {
  use <- defer(fn() { io.println("Goodbye") })
  io.println("Hello!")
}

It could be used to replicate Elixir’s for comprehensions

import gleam/list

pub fn main() {
  use letter <- list.flat_map(["a", "b", "c"])
  use number <- list.map([1, 2, 3])
  #(letter, number)
}

// [
//   #("a", 1), #("a", 2), #("a", 3),
//   #("b", 1), #("b", 2), #("b", 3),
//   #("c", 1), #("c", 2), #("c", 3),
// ]

Or it be used as a replacement for Gleam’s own try expression. Here’s the same example from above but with use rather than try.

pub fn attempt(result, transformation) {
  case result {
    Ok(x) -> transformation(x)
    Error(y) -> Error(y)
  }
}

pub fn main() {
  use user <- attempt(authenticate(credentials))
  use profile <- attempt(fetch_profile(user))
  render_welcome(user, profile)
}

It is a very flexible language feature, I’m looking forward to seeing what people do with it!

Prior art and thanks

This feature is largely inspired by Python’s with, OCaml’s let operators, the trailing lambda pattern in ML languages. Thank you to those excellent languages, and thank you to everyone on the Gleam Discord server who helped figure out how it should work.

After designing this feature we also discovered that similar features can be found in Koka (with expressions) and Roc (back-passing). They are both excellent languages and we feel we are in great company with them.

Gleam is made possible by the support of all the kind people and companies who have very generously sponsored or contributed to the project. Thank you all!

Thanks for reading! Happy hacking! 💜