My Favorite Gleam Feature
I found myself liking Gleam’s syntax more than any other language that I’ve used.With the specific exception of Elixir, where many of these insights took root. I chose to focus this article on Gleam, but both Elixir and Gleam share this feature. This article follows the path of logic as I tried to unravel why.
My journey to understanding started with a simple question:
Why are Gleam’s functions backwards compared to Erlang and Python? Specifically, why are the arguments to Gleam’s list.map
in reverse order as compared Erlang’s lists.map
?
Reverse Order
Gleam’s list.map
has the following signature:Slightly simplified. The actual signature has a with
label on the function argument, which allows for list.map([1,2,3], with: fn(x) { x + 1})
.
pub fn map(list: List(a), fun: fn(a) -> b) -> List(b)
Gleam takes a List
as its first argument, and a function to map over the list, second.
By contrast, here is the signature of Erlang’s lists.map
:
map(Fun, List1) -> List2
Erlang takes a function as the first argument and a list as the second argument.
Similarly, Python’s map()
looks the same way:Technically, the return type hint in Python3 would look different, but for the sake of expediency, I’m using -> iterator
.
map(function, iterator) -> iterator
In order to understand why Gleam is backwards, we need to understand why Erlang and Python are forwards. Why are Erlang’s and Python’s functions arranged that way?
Partial Application
Haskell shows a justification for why they’re created that way.And it is the other way, Haskell’s map
is arranged like Erlang’s.
In Haskell, all functions are implicitly curried. That is, they’re secretly all functions that take one argument and return other functions.
This makes it easy to partially apply a function:
mapPlusOne = map (+1)
Here, I’ve created a mapPlusOne
function that takes a list of numbers and returns a list of those numbers with 1 added to them. Haskell’s currying means that I can accomplish this by simply putting the two functions next to each other.
In this way, map
is “partially applied” and instead of getting back a list, I’ve created a useful function that I can reuse.
Partial application is most useful when the most generic part of the function is set in the first argument, getting more specific as you go to the right.
This makes it easy to “cut off” a function in a partially applied but generally useful state.
By contrast, if map
was arranged like Gleam, then the partial application would be less useful:
mapOnSpecificList = map [1,2,3] --insert function here
It’s far more likely that I’ll want to vary the input to a specific modification function, than to vary the modification function to a specific input. Data often changes more quickly than logic.
Python is following in these footsteps and provides functools.partial
to accomplish similar things to Haskell:I’m editorializing a bit, functools.partial
wasn’t added to Python until version 2.5 in 2006.Python’s map()
has always been explicitly for functional programming, as noted in the release notes for Python 1.0.0: “New built-in functions map()
, filter()
and reduce()
perform standard functional programming operations”.Interestingly, Lisp introduced a mapList
function in 1959, but it took the arguments in Gleam’s order: mapList(L,f)
.Nearly every other language uses the map(function, list)
order, as shown on Wikipedia.From this table, only Elixir, Haxe, R, and XQuery have non-method functions in Gleam’s order.
from functools import partial
map_plus_one = partial(map, lambda x: x + 1)
Erlang seems to have been influenced by the same ideas.
Okay, now we understand why Erlang’s functions are the way they are, why are Gleam’s functions backwards?
Pipe Operator
Gleam has a pipe operator |>
that will take the value on the left, and try to pass it in as the first argument of the function on the right. It looks like this:
[1,2,3] |> list.map(fn(x) { x + 1})
Gleam’s list.map
takes two arguments, in the example above we’re able to write list.map(fn(x) { x + 1})
because the pipe operator is silently filling the first argument with the list on the left side. This becomes a bit clearer if we use Gleam’s capture operator syntax:As noted in the language tour, this is such a common operation that the former is simply a special shorthand for the latter.In Elixir, the pipe operator is literally just a macro that takes [1,2,3] |> a_function(other, args)
and replaces it with a_function([1,2,3], other, args)
during compilation.
[1,2,3] |> list.map(_, fn(x) { x + 1})
This has the same effect as calling the function directly with that value:
list.map([1,2,3], fn(x) { x + 1})
The pipe operator has the benefit of making data transformations more “sentence”-like. Since the operator moves left to right, a series of pipe operators can transform data in the same direction as native English speakers read language:
[1,2,3]
|> list.map(fn(x) { x + 1})
|> list.filter(fn(x) { x > 2 })
|> list.at(1) // Ok(4)
Haskell, by contrast (often)It’s worth noting that Haskell has a left-to-right composition operator >>>
and a reverse application operator which works like a pipe &
, but in practice I don’t see it being used anywhere near as pervasively as the pipe operator in Gleam. ends up having a right to left flow to its logic. The specific details are finalized on the right, and then transformed by functions as the calls are made to the left:Haskell’s equivalent to the list.at
function is the !!
operator. By default all operators in Haskell are infix, but they can be turned into prefix form by wrapping them in parentheses. In this case, I also partially apply !!
with index 1.The !!
operator is different from list.at
in one way: Gleam’s version returns a result type, whereas Haskell’s version raises an error on an index missing from the list. I did find maybeAt which returns a Maybe
, Haskell’s version of result, but it’s in a library in not in Haskell’s standard library.
(!! 1)
. filter (>2)
. map (+1)
$ [1,2,3]
-- 4
While I do love the pipe operator, we still haven’t answered the question: Why are Gleam’s functions backwards? To understand that, we need to explore another property of Gleam.
“method”-like functions
There are other languages that don’t have the pipe operator that do have this left to right directionality to data transformations.
Method chaining gives us a similar ability to read transformations left to right. For example in Javascript:Here’s the documentation for the Array map
method. The other two methods are similar.
[1,2,3]
.map(x => { return x + 1})
.filter(x => { return x > 2})
.at(1);
So before, when I said that Gleam’s functions are backwards compared to Python, that was technically true. But in practice, methods are far more common in Python than functions. Python method signatures all begin with self
, similarly Rust’s methods also begin with self
as their first given argument.
For example, Rust’s Iterator.map
method has this signature:This signature has been simplified for clarity, you can confirm it against the real signature here.
fn map<B, F>(self, f: F) -> Map<Self, F>
where
F: Fn(Self::Item) -> B
It returns type Map
because it’s lazy, but otherwise the signature looks a lot like Gleam’s list.map
.
Looking more closely at Gleam, we see a similar pattern: all of the functions in gleam/list
take a value of type List
as their first argument. And nearly all them return a List
allowing pipe operator “chaining” to happen. In this way, Gleam’s functions are “method”-like, in that their signatures line up with what we would expect from method signatures in Python or Rust.The lack of this design in command line applications is part of why unix’s xargs
exists, to “fix” pipelines with the arguments the wrong way around.
However, there is a substantial difference between Gleam and these method chaining languages: Gleam doesn’t have methods.
Lack of methods
GleamAnd Elixir. doesn’t have methods. All functions are associated with modules, and not with specific data. This comes as a consequence of Gleam’s default immutability.Gleam’s Erlang target allows you to manage state with living processes, and its Javascript target has support for mutable arrays. When I pass a list to list.map
, it returns a new list without mutating the original.
This makes it easier to write functions that have referential transparency, which means that a function given the same inputs will always have the same output. This makes it easy to reason about Gleam code, since seeing the inputs and logic of the function is enough to see what the function is going to do.
The net result of this lack of methods, is that the primary way to compose code in Gleam is to use the pipe operator.There’s also the use
expression, but that’s primarily syntax sugar for working with callbacks.This lack of options is seems to be by design. Gleam has one happy path for doing things and not much room for alternate conventions. This ends up working in favor of my favorite feature.
So now Gleam has a pipe operator, and makes it the primary way to compose code, there’s one more quality of Gleam that enables my favorite feature:
A culture of qualified imports
By default an import
declaration in a Gleam module is qualified. That means that instead of dumping the functions and types from the imported module into the the current module’s namespace, they’re accessible by prefixing the imported modules name:
import other_module
pub fn main() {
other_module.useful_function(...)
}
To prevent this prefixing from getting unwieldy, Gleam will use the last part of the import path as the namespace:In Elixir, all modules are always available at their fully qualified name Module.Path.Here
, and you need to use the alias
macro in order to shorten that prefix.For example: alias Module.Path.Here
will allow that module to be used as Here
.I prefer Gleam’s design here.In fact, Gleam forces you to use the shortened prefix: gleam/list.map
will raise an error. It does allow the as
keyword to rename this qualified import if there’s a conflict.
import gleam/list
pub fn main() {
// instead of gleam/list.map
[1,2,3] |> list.map(...)
}
Gleam does allow for unqualified imports, where functions can be used directly the current modules namespace, but they must be enumerated explicitly:
import gleam/list.{map, filter}
pub fn main() {
[1,2,3]
|> map(fn(x) { x + 1})
|> filter(fn(x) { x > 2})
}
However, Gleam warns against unqualified imports in the language tour:
This may be useful for values that are used frequently in a module, but generally qualified imports are preferred as it makes it clearer where the value is defined.Emphasis mine.
It is the combination of these three factors: a language designed around pipe operator, a lack of methods in the language, and a culture of qualified imports that leads to my favorite feature of Gleam:
My Favorite Feature: Discoverability
As a consequence of these three factors working in combination, I can visibly see which modules each function in a data transformation pipeline come from. For example in:
import gleam/list
[1,2,3]
|> list.map(...)
|> list.filter(...)
|> list.at(...)
I know where map
and filter
and at
come from, and I can easily look up the documentation for gleam/list
to understand more about each function and what other functions are available. The implementation details of a function in Gleam are plain to see and easy to learn more about.
So my favorite feature of Gleam, is that it’s discoverable. I can read someone else’s code and understand each every module that they chose to use, and where every function comes from.In a language with method chaining, a Language Server grants you write-time discoverability. You can add a .
and hit tab, and discover all of the currently available methods.However, at read time, this discoverability is lost, making it hard to understand which trait or type or object or class implements the method that you’re currently dealing with.Often classes will reuse method names, overloading a given term, and making harder to search for where that method is implemented.
This discoverability makes it a joy to read source code written in Gleam. I’ve improved as a software engineer as a consequence of being able to learn from the core libraries of my favorite language.
Thank you to John, Theodore, Nicole, Jeff, Miccah, and Jake for helping me write and edit this article. And thank you to the Gleam discord for helping me validate some of these concepts.