Things I like about Gleam’s Syntax
Gleam is a Type-Safe programming language that transpiles to ErlangAnd javascript., so you can run its code on the BEAM. I’ve slept on Gleam for a while because its syntax. It was too much Rust and not enough Elixir for me.
Recently, I was reminded of Gleam when someone mentioned that Gleam doesn’t have an if statement, but instead uses case
for everything. As a consequence, I ended up re-reading the Gleam language tour.
As another reminder to check my assumptions from time to time, Gleam’s syntax has some goodies that I hadn’t noticed before, It’s definitely grown on me now that I’ve used Rust for a while.Gleam’s syntax draws a lot from Rust’s syntax. Gleam’s transpiler is written in Rust.
Some examples:
Handy pipe operator
Gleam adopts a pipe operator similar to ElixirGleam’s list.at
function returns a Result
type, which allows me to know whether I’ve received a value from the list or used an index outside the bounds of the list. The former will be Ok(value)
and the latter will return Err(Nil)
per the type signature of list.at
.This is in contrast to Elixir’s Enum.at/2
function, which returns value
or nil
with no indication of whether that nil
came from the list or from an out-of-bounds index.:
import gleam/io
import gleam/list
pub fn main() {
[1, 2, 3]
|> list.append([4,5,6])
|> list.reverse
|> list.at(2)
|> io.debug() // prints Ok(4)
}
Labeled arguments
Labeled arguments is how Gleam does keyword arguments which are notably missing from both Elixir and Rust. So this is a great addition. The main benefit of labeled arguments is that you can pass them into the function in any order, since they’re identified by name.Note that I had to use name
twice in greet
. The first time is the label, the second time is the variable I use in the body of the function. Using one name
by itself raises a compiler error.Elixir can simulate keyword arguments with keyword lists, but those require a Keyword.get
call to retrieve keys inside the function body, and can’t be passed in positionally.
import gleam/io
fn greet(name name: String, in place: String) {
"Hello " <> name <> " from " <> place
}
pub fn main() {
greet(name: "Christine", in: "NASA")
|> io.debug() // "Hello Christine from NASA"
}
Elegant function capturing
Function capturing allows me to quickly create a one-argument anonymous function from a named function. The one argument will go wherever I place the _
in the function capture. It only seems to work for creating one-argument anonymous functions,
putting multiple _
in a capture will compile errorBy contrast, in Elixir you can have arbitrarily many arguments in a captured function with the numbered &
syntax: &Enum.reduce([1, 2, 3], &1, &2)
(At least, I think it’s arbitrarily many, I stopped checking after &11
worked).
import gleam/io
import gleam/list
import gleam/string
pub fn main() {
let prepend = string.append("other ", _)
list.map(["hello", "world"], prepend)
|> io.debug() // ["other hello", "other world"]
}
Function capturing is also nice for arranging functions for the pipe operator, since Gleam lets me pipe directly into an anonymous functionElixir does not let you pipe directly into an anonymous function. In Elixir 1.12.0, then/2
was added to remedy this problem.:
import gleam/io
import gleam/string
pub fn main() {
"hello"
|> string.append("some text ", _)
|> io.debug()
}
Only case statement, no if
Gleam doesn’t have an if statement, it uses case for that insteadGleam does use if
as a keyword, but only in guard expressions.:
case if_boolean {
True -> "case statements only"
False -> "you didn't get a choice"
}
I like this syntax better than an if statement. It shines a spotlight on the branch, makes it clear which path belongs to which condition, and Gleam will do exhaustiveness checking to force me to handle both cases.
It also makes it easier to refactor into a sum type:
type CaseExample {
CaseGood
CaseAnyway
ThirdOption
}
case case_example {
CaseGood -> "case statements only"
CaseAnyway -> "you didn't get a choice"
ThirdOption -> "more choices!"
}
Sub pattern names using as
Gleam lets me assign names to sub-patterns using as
in pattern matching expressions. Elixir solved this problem by making =
expressions return their value, so you could use them in pattern matching, but I always found that somewhat awkward. as
makes it clear that I’m naming a sub-pattern for reuse.
case list {
[[_, ..] as inner_list] -> inner_list
other -> []
}
Note that Gleam makes it easy to destructure types with custom variables, avoiding the need for as
:
// example, use gleam/option.Option instead
type OptionalString {
Some(string: String)
None
}
fn unpack_string(optional_string) {
case optional_string {
Some(chosen_variable) -> chosen_variable
None -> ""
}
}
Succinct alternative clause patterns using |
Gleam lets you succinctly specify alternate patterns in a case statement using |
:
type NumberKind {
Prime
Composite
}
case number {
2 | 3 | 5 | 7 -> Prime
_ -> Composite
}
Support for todo
to mark uncompleted work
Gleam includes a todo keyword to indicate that some code isn’t finished. That code will type check, but will raise a warning on compilation:
fn fancy(something: SomeComplexType) {
todo
}
The warning:
This code will crash if it is run. Be sure to finish it before running your program.
Gleam lets me add a description to the todo
using as
:
import gleam/io
pub fn main() {
let incomplete = fn(x) {
case x {
2 | 3 | 5 | 7 -> "Prime"
_ -> todo as "Something about composite numbers"
}
}
incomplete(7)
|> io.debug()
}
Takeaways
I really like what Gleam has done with its syntax. Despite my initial misgivings, I’m finding it hard not to play with Gleam as I write this article.
If Gleam’s syntax initially put you off, consider giving it another look. I know I like what I found.