/ 10 min read
Advent of Code 2024: A Gleam Retrospective
This year (2024), I did the Advent of Code challenges/games. It’s essentially a daily programming puzzle challenge, spanning over the advent period (December 1 - 25).1 Since I have extensively learned Rust and Zig early on in the year, and since I write Python professionally, I decided to use something new for this.
Because of Rust, I became to like the ML family of languages (think of OCaml, not machine learning). My choice landed upon Gleam, a functional language that is quite similar in look-and-feel as Rust. Programming in Gleam does feel like programming in Rust but in a strict, functional way: There’s no escape hatch to fall back to procedural programming with mutability.
This post is a “great-bad” (👍-👎) evaluation of Gleam for solving programming puzzles. At the end, I will also give my opinion on what Gleam is currently lacking from the perspective of a machine learning scientist.
👍 I wish every language had a pipeline operator
Suppose we have
- compose the functions, then apply
: , or - apply the function to the previous function’s output:
.
Back to the programming world, usually we do the latter and we do so by calling f_n(f_n_1(... f_1(x)))
.
But it can be cumbersome to match the parens.
Even more so if
In Gleam (and other functional languages), there is a special operator called the pipeline operator |>
.
The above example can then be written as x |> f_1 |> f_2 |> ... |> f_n
.
Neat!
Here’s an example from my AoC 2024 solutions code:
As we can see, the pipeline operator is a very nice way to “carry” an input through the “pipeline” to obtain the result at the opposite end.
Traditionally the pipeline operator will put the output of the previous function to the first input of the next function. However, in Gleam, we can put an output into an arbitrary input slot of the next function:
That is, _
marks the “slot” we want to put the l.h.s. of the pipeline operator into.
👍 The “let assert” keyword is great for rapid prototyping
When writing a Rust code, I often do so in multiple passes.
First, a prototyping phase where I use the .unwrap()
and if-let-else-panic
snippets extensively to validate my algorithms quickly without worrying about safety.
Then, the second phase is to refactor the above carefully, and handle all cases/errors correclty instead of throwing panic everyime.
However, in programming challenges like AoC, we don’t really need to robustly handle error — all that matters is correctness. So it’s useful to just stop after the first phase.
How can we do this in Gleam? For sure we don’t really want to handle all pattern matching cases exhaustively for AoC. For instance, if we know the input for the puzzle is a file with two text blocks separated by an empty line, we process this input by:
For the sake of robustness, the code will be quite long and nested.
Thankfully, Gleam has a special keyword let assert
to alleviate this.
Think of it as .unwrap()
and if-let-else-panic
in Rust:
If something doesn’t match the asserted pattern, it will immediately panic.
So, the code above can be written as:
Much more concise!
👍 The “use” expression is a godsend
I wrote about Gleam’s use
expression in the previous post, where I approached the discussion from Python’s with
-block perspective.
However, it can also be combined with the stdlib functions bool.guard
and bool.lazy_guard
to emulate early returns in procedural languages and to easily emulate Python’s decorator.
Emulating early return
In Rust, we can do:
Notice that the first function is cleaner.
This will be even more apparent if inside the else
block we have a very long code.
The standard functional counterpart would be:
Imagine if we have nested if
’s.
The nestings, indentations, and curly brackets in the above Gleam code will make the code quite ugly and hard to debug.
Luckily, the use
expression, combined with bool.guard
can rescue us:
Notice that we have no nesting now! To drive home the point, consider this nested-if code:
I’m already tired just looking at that code 😩.
With use
and bool.guard
we can achieve a cleaner look:
No indentation, no nesting 🥳!
As a final note, be attentive to the return value of your early return.
If the return value is a result of a dynamic computation, bool.lazy_guard
should be used instead.
The difference is that the second argument (previously, the return value e.g. False
) is now a callback function that will be executed lazily when the first argument evaluates to True
.
Emulating Python decorator
Since AoC is a programming puzzle, dynamic programming (DP) questions often come up. In functional language, it’s natural to write a top-down DP since it amounts to recursion and memoization. However, memoization is cumbersome in functional language due to immutability. For example, in Python, we can do:
As we can see, we can have a clean code by defining memo
as a global, mutable variable.
Moreover, this can be simplified further by using functools.cache
decorator: We write the standard, non-memoized version, and just slap @functools.cache
on top.
But in Gleam, we have to do this:
We use list.map_fold
since we have to keep track of the state of memo
and reuse it for the next function calls of fibo
.
It has quite a bit of boilerplate and we must return memo
together with the result in the fibo
function.
The result is a longer and less clean code.
Thankfully, the use
expression enables us to emulate Python’s decorator, which @functools.cache
is.
Someone has thought of this and it’s available here, as a rememo
library.
Then we can write this cleaner code:
Looks great 🤩🤩🤩!!!
👎 No operator overloading
Suppose I have an integer x
and a float y
and I want to add them together.
In most contemporary languages, you can just write x + y
.
But this cannot be done in Gleam since integer addition and float addition are two different operators: +
and +.
, respectively.
There is a good reason for this, just like in OCaml.
But the end result is a more verbose, out-of-the-ordinary (and thus perceived as unnatural) code:
I could imagine that due to this, you cannot have a concise expression like tensor_1 + tensor_2
for some ndarray
types defined in a hypothetical Gleam tensor library.
All in all, I don’t think this will help Gleam if it wants to be widely adopted by people from various scientific communities.
👎 Barebone ecosystem
On Day 13 of AoC 2024, the puzzle can be solved by solving a system of linear equations. In Python, there is Numpy, PyTorch, etc., giving rise to a concise solution:
But what about Gleam?
I don’t see any robust, full-featured ndarray/tensor library in Gleam.
OCaml has owl
, Elixir has nx
, Gleam has none.
Not only that, pandas
-like and matplotlib
-like libraries are also missing.
From what I’ve gathered, Gleam is fully interoperable with other languages in the Erlang virtual machine.
This means Gleam can actually call Elixir libraries such as nx
for ndarrays, explorer
for dataframes, and VegaLite
for plotting.
However, it seems one needs to explicitly “translate” every Elixir function into Gleam first before using it.
For example: 2
I can then call hello_elixir()
in my .gleam
file and it will, in turn, call the specified Elixir code.
It would be great if we could just call Elixir code without translating it first: Just add Elixir dependencies, then call.
Or, better yet, we need Gleam bindings for all those aforementioned libraries.
Without this, I don’t think Gleam can sway Python’s ML or data science community.
Conclusion
Gleam is great and is a breath of fresh air coming from Python! I wish I could just use Gleam for all my research work but alas.
Actually, after writing this post, I became even more intrigued with Elixir. It might scratch my itch of moving away from Python due to less “quirky” design decisions and due to its more mature, vibrant scientific computing ecosystem. It’s a shame that I can’t move to Gleam (just yet, hopefully), though.
Footnotes
-
In the Christian tradition, the advent calendar is essentially used to count the days in anticipation of Christmas. ↩
-
Taken from https://github.com/gleam-lang/mix_gleam ↩