My Favorite Gleam Feature
I found myself liking Gleam’s syntax more than any other language that I’ve used. 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: 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: 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. 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: 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: [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) 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: (!! 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: [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: 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. However, there is a substantial difference between Gleam and these method chaining languages: Gleam doesn’t have methods.
Lack of methods Gleam 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. 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. 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: 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. 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: