Published 24 Nov, 2022 by Louis Pilfold
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!
- Aaron Gunderson
- Adam Brodzinski
- Adi Iyengar
- Alembic
- Alex Manning
- Alex Rothuis
- Alexander Koutmos
- Alexandre Del Vecchio
- Ali Farhadi
- Anthony Khong
- Anthony Scotti
- Arnaud Berthomier
- Arno Dirlam
- Ben Marx
- Ben Myles
- brettkolodny
- Brian Glusman
- Bruno Michel
- Bryan Paxton
- Carlos Saltos
- Charlie Duong
- Chew Choon Keat
- Chris Lloyd
- Chris Young
- Christian Meunier
- Christopher Keele
- clangley
- Clay
- Cole Lawrence
- Colin
- Copple
- Cristine Guadelupe
- Damir Vandic
- Dan Dresselhaus
- Dan Mueller
- Dave Lucia
- David Armstrong Lewis
- David Bernheisel
- David Flanagan
- Dennis Tel
- dependabot[bot]
- Edon Gashi
- Elliott Pogue
- Eric Meadows-Jönsson
- Erik Terpstra
- Fernando Farias
- Filip Figiel
- Florian Kraft
- fly.io
- Graeme Coupar
- Guilherme de Maio
- Gustavo Villa
- Harry Bairstow
- Hayleigh Thompson
- Henry Warren
- human154
- Ian Gonzalez
- Ingmar Gagen
- inoas
- Ivar Vong
- James MacAulay
- Jan Skriver Sørensen
- Jen Stehlik
- jiangplus
- John Gallagher
- John Palgut
- Jonathan Arnett
- josh rotenberg
- José Valim
- Josías Alvarado
- João Veiga
- Julien D
- Justin Blake
- Kapp Technology
- Kayla Washburn
- Kieran Gill
- Lars Wikman
- Lauro Figueiredo
- lidashuang
- Marcel Lanz
- Marcin Puc
- Marius Kalvø
- Mark Holmes
- Mark Markaryan
- Markus
- Martin Janiczek
- Mathias Jean Johansen
- Matt Van Horn
- Max Lee
- Michael Chris Lopez
- Michael Davis
- Michael Jones
- Michał Kowieski
- Michał Łępicki
- Michele Riva
- Mike Lapping
- Mike Roach
- Milton Mazzarri
- mklifo
- Nathaniel Knight
- Nick Reynolds
- Nicklas Sindlev Andersen
- NineFX
- OldhamMade
- Ole Michaelis
- Oliver Searle-Barnes
- ontofractal
- paca94
- Parker Selbert
- Pete Jodo
- porkbrain
- Praveen Perera
- qingliangcn
- Quinn Wilton
- Raúl Chouza
- Redmar Kerkhoff
- Reio Piller
- Rico Leuthold
- rikashore
- Robert Attard
- Robert Ellen
- Rodrigo Oler
- Ryan Winchester
- Sam Aaron
- Sascha Wolf
- Saša Jurić
- Scott Wey
- Sean Jensen-Grey
- Sebastian Porto
- Shunji Lin
- Shuqian Hon
- Signal Insights
- Simone Vittori
- Stefan Luptak
- Strand Communications
- Szymon Wygnański
- t bjornrud
- Terje Bakken
- Tomasz Kowal
- Tomochika Hara
- Tristan de Cacqueray
- Tristan Sloughter
- Tyler Rockwood
- tynanbe
- Uku Taht
- Vincent Le Goff
- Weizheng Liu
- Wilson Silva
- Wojtek Mach
- YourMother-really
- Yu Matsuzawa
- zaatas
- Zsombor Gasparin
Thanks for reading! Happy hacking! 💜