Hi, I’m Erika Rowland (a.k.a. erikareads). Hi, I’m Erika. I’m an Ops-shaped Software Engineer, Toolmaker, and Resilience Engineering fan. I like Elixir and Gleam, Reading, and Design. She/Her. Constellation Webring Published on Modified on

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.


Constellation Webring