Gleam is a type-safe and scalable language for the Erlang virtual machine and JavaScript runtimes. Today Gleam v1.11.0 has been published.

30% faster? Really?

The title of this article makes a bold claim: Gleam compiled to JavaScript being 30% faster! Gleam doesn't add any additional runtime when compiling to JavaScript, and the generated code is very much like the JavaScript a human would write, so this is an accomplishment we're very happy with.

First, a benchmark. Lustre is a frontend web framework for Gleam, capable of both SPA style and LiveView style functionality. It has a virtual DOM implementation with comparable performance to established frameworks in other languages, such as React and Elm. It is one of the most widely used Gleam packages, and certainly the most widely used Gleam web framework.

Lustre's virtual DOM diffing is implemented in Gleam, so improving its performance will benefit many Gleam users, and it will serve as a good benchmark to demonstrate the improvements. The charts below show the number of operations per second for diffing HTML tables of various sizes (higher is better).

10 rows

  1. v1.10.0
    142k
  2. v1.11.0
    208k

100 rows

  1. v1.10.0
    15.9k
  2. v1.11.0
    21.5k

1000 rows

  1. v1.10.0
    1.5k
  2. v1.11.0
    2.1k

Without any modification to Lustre itself it has got 30% faster! All other Gleam projects compiling to JavaScript can expect to similar performance improvements, with greater improvements to projects with more complex pattern matching.

OK, how does it work?

Gleam has a single flow control construct, the case expression. It runs top-to-bottom checking to see which of the given patterns match the value.

pub fn greet(person: Person) -> String {
  case person {
    Teacher(students: [], ..) -> "Hello! No students today?"
    Student(name: "Daria", ..) -> "Hi Daria"
    Student(subject: "Physics", ..) -> "Don't be late for Physics"
    Teacher(name:, ..) | Student(name:, ..) -> "Hello, " <> name <> "!"
  }
}

Prior to this release, when compiling to JavaScript code results in if else chain, as can be seen in the code below.

export function greet(person) {
  if (isTeacher(person) && isEmpty(person.students)) {
    return "Hello! No students today?";
  } else if (isStudent(person) && person.name === "Daria") {
    return "Hi Daria";
  } else if (isStudent(person) && person.subject === "Physics") {
    return "Don't be late for Physics";
  } else if (isTeacher(person)) {
    return "Hello, " + person.name + "!";
  } else {
    return "Hello, " + person.name + "!";
  }
}

Disclaimer: This code has been lightly edited for clarity, but all aspects related to this implementation change remain the same.

This is very understandable and human looking code, but it's not as efficient as possible. For example, if the value is a Student with a name other than "Daria" and a subject other than "Physics" then the isTeacher and isStudent functions are each called twice, resulting in wasted work. Similar problems arise with other patterns, especially with the list type as it would need to be traversed multiple times to check various elements within them.

The new and improved approach is to transform the linear sequence of checks into a decision tree, where each check is performed the minimum number of times to find the matching clause as quickly as possible. This decision tree is then compiled into a series of nested if else statements in JavaScript.

export function greet(person) {
  if (isTeacher(person)) {
    if (isEmpty(person.students)) {
      return "Hello! No students today?";
    } else {
      return "Hello, " + person.name + "!";
    }
  } else {
    if (person.name === "Daria") {
      return "Hi Daria";
    } else {
      if (person.subject === "Physics") {
        return "Don't be late for Physics";
      } else {
        return "Hello, " + person.name + "!";
      }
    }
  }
}

This does result in a small increase in code size (up to 15% in our tests), but the uniform nature of the extra code is well suited to compression, with minification and brotli compression completely removing this extra size and producing the same application bundle size as with previous Gleam versions.

We have only implemented this optimisation for the JavaScript target and not the Erlang one as the Erlang Virtual machine implements this optimisation itself! It is done for us automatically there.

As part of this work the compilers analysis of pattern matching was enhanced, notably around bit-array patterns. It can now identify when a clause with a bit array pattern is unreachable because it only matches values that a previous clause also matches, such as the second clause here:

case payload {
  <<first_byte, _:bits>> -> first_byte
  <<1, _:bits>> -> 1
  _ -> 0
}

Efficient compilation of pattern matching is a surprisingly challenging problem, and we would not have had so much success without academic research on the subject. In particular we would like to acknowledge "How to compile pattern matching", by Jules Jacobs and "Efficient manipulation of binary data using pattern matching", by Per Gustafsson and Konstantinos Sagonas. Thank you.

This is the culmination of work that was started before Gleam v1, and has been desired for much longer. A huge thank you to Giacomo Cavalieri for this final piece.

But that's not all! There's lots more new things in this release. Let's take a look.

Testing with assert

Types and static analysis are wonderful tools to help you write code, but you're always going to need to test programs to know they are working correctly. Gleam the language historically doesn't have any built-in testing functionality, so test libraries define assertion functions that can be used in tests.

pub fn hello_test() {
  telephone.ring()
  |> should.equal("Hello, Joe!")
}

This has worked well enough to be productive, but it's not the world-class experience we want in Gleam. These assertion functions are just that: they are functions. All they can do is take arguments and return values. They can't know anything about the code that produced those arguments, or the context from which they are being called. The debugging information that a Gleam test framework can provide with this is quite limited to compared to other languages that have either built-in assertion capabilities, a macro system, or other similar features.

Gleam isn't a language about types, it's a language about productivity and developer joy. This sub-par testing experience wasn't up to our high standards, and with this release we've made a big step towards correcting that with the addition of assert.

pub fn hello_test() {
  assert telecom.ring() == "Hello, Joe!"
}

This new syntax panics if the given expression evaluates to False. What makes it different to conditionally calling the existing panic syntax is that the runtime error is annotated with information about the expression that evaluated to False, so test frameworks can provide detailed debugging information in the event of a test failure.

Here's the output that the gleeunit test framework will use. Other Gleam test frameworks may go further with even better formats!

...............
assert test/my_app_test.gleam:215
 test: my_app_test.hello_test
 code: assert telecom.ring() == "Hello, Joe!"
 left: "Hello, Mike!"
right: literal
 info: Assertion failed.
..........
25 tests, 1 failures

Note how the test framework has enough information to show the assertion code as it was written in the source file, and can show the values for the left and right of the == operator.

As well as operators it can also understand function calls and show what each argument is.

........
assert test/my_app_test.gleam:353
 test: my_app_test.system_test
 code: assert telecom.is_up(key, strict, 2025)
    0: "My WIFI"
    1: True
    2: literal
 info: My internet must always be up!
.................
25 tests, 1 failures

In that example there was a custom assertion message, displayed under the info key. This is provided the same way as custom messages for Gleam's todo and panic syntaxes, using the as keyword.

pub fn system_test() {
  let key = "My WIFI"
  let strict = True

  assert telecom.is_up(key, strict, 2025)
    as "My internet must always be up!"
}

Upgrading from gleeunit's assertion functions to the new assert syntax is a slow and tedious job for a human to do, so Gears has made a helpful command line tool to do it automatically! Run it and your codebase will be updated for you in just a second.

Thank you Surya Rose for this! It will make a big difference to all Gleam programmers testing their programs.

gleam dev

A Gleam application typically has two main functions, one in src/ for running the application, and one in test/ for running the tests. The application main function can be run with the console command gleam run, and the test one with the command gleam test. This is easy enough to understand, but what if you have some other code you need to run in development? For example, if you're making a backend web application you might want to have some extra code that configures a local development database, or compiles some frontend assets. Where would you put the code that does this?

This is development code that you do not want to ship to production (extra code and dependencies is a potential security risk), and having code that can make destructive changes to the database in production is an accident waiting to happen!

The traditional wisdom is to put a new module and main function the test/ directory (as this code isn't included in production) and then to run it with gleam run --module $THE_MODULE. Placing non-test code in a directory called test isn't very intuitive, so it's not uncommon for people to place it in src instead, resulting in development code and dependencies accidentally being published.

This release adds a new source directory for development code, dev/. Code in the dev/ directory can import src/ code and use development dependencies, and the $PACKAGENAME_dev module's main function can be run with the new gleam dev console command. This should hopefully be a more satisfying and easy to understand system, preventing future mistakes.

Thank you Surya Rose!

Help with understanding immutability

Gleam is an immutable language, which means that values are not updated in-place, instead new values are constructed from older values, with the desired changes applied. If you are familiar with mutable languages this may seem inefficient, but immutable languages apply some clever optimisations to get excellent performance and memory usage.

It's possible for people new to the immutable style to get confused, accidentally discarding the new updated version of some data. For example:

pub fn call_api(token: String) -> Response(String) {
  let req = sdk.new_api_request()
  request.set_header(req, "authentication", "Bearer " <> token)
  http_client.send(req)
}

Can you spot the bug?

The set_header function returns a new request value, but it is not assigned to a variable, so the original request value without the authentication header is sent instead. The bug is fixed by passing the output of each function into the next one.

pub fn call_api(token: String) -> Response(String) {
  sdk.new_api_request()
  |> request.set_header("authentication", "Bearer " <> token)
  |> http_client.send
}

To help avoid this mistake the compiler will now emit a warning when a function without any side effects is called but the return value isn't used. For example the following code:

fn go() -> Int {
  add(1, 2)
  add(3, 4)
}

Will produce the following warning:

warning: Unused value
    ┌─ /src/main.gleam:4:3

  4 │   add(1, 2)
   ^^^^^^^^^ This value is never used

This expression computes a value without any side effects, but then the
value isn't used at all. You might want to assign it to a variable, or
delete the expression entirely if it's not needed.

Thank you Surya Rose!

JavaScript bit array improvements

Gleam has a powerful literal syntax for constructing and parsing binary data, a much loved feature common to BEAM languages. Gleam supports this syntax when compiling to Erlang or to JavaScript, but some aspects of the syntax were not yet usable on JavaScript. This release adds support for UTF-16 and UTF-32 encoded segments of data.

Thank you Surya Rose!

Playing nicely with POSIX

The Gleam build tool has a gleam export erlang-shipment command for compiling and preparing a project for deployment to a server or similar. It includes a script for starting the Erlang virtual machine and running the program, but unfortunately it was written in a way that meant that the program would not receive POSIX exit signals.

Christopher De Vries has fixed this problem. Thank you Christopher!

Generated documentation improvements

When publishing a package to Hex, the BEAM ecosystem package repository, the Gleam build tool will generate and upload HTML documentation for the code.

When generating documentation, the build tool now prints type variable using the same names as were used in the source code, making it easier to understand what these type parameters are. For example, previously this function would have been rendered like this:

pub fn from_list(entries: List(#(a, b))) -> Dict(a, b)

But now is rendered as the following:

pub fn from_list(entries: List(#(key, value))) -> Dict(key, value)

Another area of the documentation that has been improved is how types imported from other modules are displayed. These types are now displayed with their module qualifiers, and hovering over them shows the full module name. For example, this code:

import gleam/dynamic/decode

pub fn something_decoder() -> decode.Decoder(Something) {
  ...
}

Will now generate the following documentation:

pub fn something_decoder() -> decode.Decoder(Something)

Hovering over the decode.Decoder text will show the following:

gleam/dynamic/decode.{type Decoder}

Clicking on decode.Decoder will take you to the documentation for that type.

Thank you Surya Rose!

Yet more fault tolerance

Gleam's compiler implements fault tolerant analysis. This means that when there is some error in the code that means it is invalid and cannot be compiled, the compiler can still continue to analyse the code to the best of its ability, ignoring the invalid parts. Because of this Gleam language server can have a good understanding of the code and provide IDE feature even when the codebase is in an invalid state.

This release makes the analysis of lists, tuples, negation operators, panic, echo and todo, function parameters, and function labels fault tolerant. We're covering all the last remaining little case to make the language server information as fresh and accurate as possible!

Thank you Giacomo Cavalieri and Surya Rose!

Labels in inexhaustive case expression errors

Gleam has exhaustiveness checking. This means that pattern matching flow control must handle all the possible values that the type being matched on could be. If there are any clauses missing then the program is incomplete, and the compiler will return a helpful error showing the missing patterns.

This error has been improved to include record field labels, as can be seen at the bottom of this example:

Inexhaustive patterns: Unused value
  ┌─ /src/main.gleam:6:3

6 │ ╭   case person {
8 │ │     Teacher(name:) -> io.println("Good morning!")
7 │ │     Student(name: "Samara", age: 27) -> io.println("Hello Samara!")
9 │ │   }
 ╰───^

This case expression does not have a pattern for all possible values. If it
is run on one of the values without a pattern then it will crash.

The missing patterns are:

    Student(name:, age:)

This improvement has also upgraded the code action to add missing patterns to a case expression, now when the missing clauses are added to the code they will include record labels.

Thank you Surya Rose!

Fill labels code action for patterns

The language server has a code action for filling in possible labels for a function or record. This can be a convenient time saver when writing code, especially if you can't immediately remember what labels the arguments use.

This code action has been upgraded to also work with records in patterns. In this code the Person record pattern is missing two of its fields, so running the code action will add them for you.

pub type Person {
  Person(name: String, age: Int, job: String)
}

pub fn age(person: Person) {
- let Person(age:) = person
+ let Person(age:, name:, job:) = person
  age
}

Thank you Surya Rose!

Bit array truncation warning

The compiler now raises a warning when it can tell that an int segment with a literal value is going to be truncated. This will help folks understand a behaviour that may be unexpected.

warning: Truncated bit array segment
    ┌─ /src/main.gleam:4:5

  4 │   <<258>>
     ^^^ You can safely replace this with 2

This segment is 1 byte long, but 258 doesn't fit in that many bytes. It
would be truncated by taking its first byte, resulting in the value 2.

Thank you Giacomo Cavalieri!

Better language server support for constants

Gleam's language server has historically been missing hover, autocompletion, and go-to definition within constants. Surya Rose has corrected this by implementing these features. Thank you Surya!

Generate function code action improvements

Gleam's language server has a code action for generating the outline of a function that is used in the code but does not yet exist.

It has been upgraded to now choose better argument names based on the labels and variables used. For example, if the code action is run on the not-yet-defined function named remove:

pub fn main() -> List(Int) {
  let list = [1, 2, 3]
  let number = 1
  remove(each: number, in: list)
//^^^^^^ This function doesn't exist yet!
}

The language server will then generate the outline of the missing function, and the code will look like this:

pub fn main() -> List(Int) {
  let list = [1, 2, 3]
  let number = 1
  remove(each: number, in: list)
}

fn remove(each number: Int, in list: List(Int)) -> List(Int) {
  todo
}

Thank you Giacomo Cavalieri!

Generate variant code action

The language server now provides a code action to automatically generate a new custom type variant, similar to the generate function code action.

In this example the UserPressedButton variant does not exist, but the compiler can tell from the way that it is used that if it did exist it would be a variant of the Msg custom type.

pub type Msg {
  ServerSentResponse(Json)
}

pub fn view() -> Element(Msg) {
  div([], [
    button([on_click(UserPressedButton)], [text("Press me!")])
    //               ^^^^^^^^^^^^^^^^^ This doesn't exist yet!
  ])
}

Triggering the code action on the UserPressedButton will add it to the Msg type:

pub type Msg {
  ServerSentResponse(Json)
+ UserPressedButton(String)
}

The code action understood the code well enough to know that this new variant holds a string value, as that is what the on_click function does with it in this example. If the variant was used with labels then the labels would be included in the definition also.

Thank you Giacomo Cavalieri! Another excellent code action for "top-down" style programmers.

Remove unused imports code action improvements

For a long time the Gleam language server has included a code action for removing unused imports, but it wouldn't work as well as desired for imports that include unqualified types and values: they would still remain after running the action.

import a_module.{type Unused, unused, used}

pub fn main() {
  used
}

Triggering the code action will remove all unused types and values:

import a_module.{used}

pub fn main() {
  used
}

Thank you Giacomo Cavalieri!

Windows ARM binaries

We provide precompiled executables for each Gleam release, supporting Windows, macOS, and Linux. Gleam users and package management systems can download and use these executables instead of compiling the Gleam project from scratch, which takes a long time.

Jonatan Männchen has added an ARM64 Windows build. This is useful as ARM based development machines are becoming more and more common. Thank you Jonatan!

And the rest

And thank you to the bug fixers and experience polishers: Ariel Parker, Giacomo Cavalieri, Louis Pilfold, Mathieu Darse, Matias Carlander, Samuel Cristobal, and Surya Rose.

For full details of the many fixes and improvements they've implemented see the changelog.

A call for support

Gleam is not owned by a corporation; instead it is entirely supported by sponsors, most of which contribute between $5 and $20 USD per month, and Gleam is my sole source of income.

We have made great progress towards our goal of being able to appropriately pay the core team members, but we still have further to go. Please consider supporting the project or core team members Giacomo Cavalieri and Surya Rose on GitHub Sponsors.

Thank you to all our sponsors! And especially our top sponsor: Lambda.

Try Gleam