A Tutorial Introduction to Gleam -- Part 1
Introduction
Gleam is a newish programming language that I recently stumbled upon. It is so little-known that there aren’t really any tutorials available on it (even on the official documentation), so I decided to write my own.
Gleam transpiles to Javascript and Erlang. I’ll be focusing my attention on the Erlang side of things, as it is more mature. And to be honest, I don’t feel any reason to replace Rescript, my go-to transpires-to-Javascript language.
Patreon
If you find my content valuable, please do consider sponsoring me on Patreon account. I write about what interests me, and I usually target tutorial style articles for intermediate to advanced developers in a variety of languages.
I’m not sure how far I’m going to go with this series on Gleam, so if you’re particularly interested in the topic, a few dollars a month definitely gives me a bit of motivation to keep it up.
Gleam In Brief
Gleam self-describes as “Safe, Friendly, and Performant”, but what language doesn’t describe itself that way? It also mentions “Erlang compatible”, which narrows it down a bit, though syntax-wise, they don’t look much alike.
A more informative description might be “Soundly Typed, Concurrent, and Functional”.
Gleam fits in the bucket of what I call “new-style functional language”. Like Rescript, it is more approachable (“friendly”) than older functional languages such as OCAML and Haskell. At first glance, it also looks more practical and pragmatic, but I haven’t used it enough to be sure of that.
Like Erlang, Gleam uses the industry-proven actor model for concurrency. Surprisngly, very little of this model is encoded into the language, instead being farmed out to libraries, which is why it is able to build to Javascript without shipping a massive runtime.
Sound interesting? I think so too. Let’s build something together.
That’s right. I’m not an expert in Gleam. I learn best by explaining things, so I’ll be learning along with you. I do assume you have some programming knowledge. I’ll probably drop comparisons to other languages (I mean, I’ve already done so, so why stop now?) all over the place, but you don’t need to know any specific one of them to understand my content.
Install Gleam
Getting programming environments set up can be a real pain, especially with languages that haven’t matured their packaging ecosystem. The Gleam Getting Started page has a nice description of how to get up and running, and all of the methods look civilized in a “welcome to our neighbourhood, we’re glad you’re here” kind of way.
For my part, brew install gleam
was all I needed.
Hello Gleam
Fire up a terminal and point it to your hobby code directory. If you don’t have a hobby code directory, make one.
“Everybody needs a hobby code directory.” – definitely not my mother.
Gleam ships with its own template generator, and I’ll give you three guesses as to how to start a new project (no peeking!):
gleam new hello_world
cd hello_world
Someone recently complained to me that “Hello World” is a stupid custom. But I’m not about to fly in the face of decades of programming tradition, and the truth is I can get you to “Hello World” without writing a line of gleam code. Don’t believe me? Try this:
gleam run
Does it say Hello from hello_world
? Why yes, I believe it does. I’m
definitely not taking credit for all the hard work the Gleam devs have done for
us here. Let’s take a look at the files generated for us:
.
├── README.md
├── gleam.toml
├── src
│ └── hello_world.gleam
└── test
└── hello_world_test.gleam
Four files. That’s comfortable; enough to get us started, but not so much that we can’t remember what’s been written for us and spend our first ten minutes deleting useless boilerplate. (That sentence was not a dig at create-react-app, but this one is).
We’ll ignore the README (does anybody ever obey that filename anyway?) and
gleam.toml
.
Let’s look at that source file, though.
// hello_world.gleam
import gleam/io
pub fn main() {
io.println("Hello from hello_world!")
}
Kinda interesting that you have to import a module just to print something to the screen. It’s not a common design choice these days, but I like it; it’s nice and explicit.
Imports look about the same as any other language (I’m not going to try to figure out the Gleam module system at this time).
pub fn
means we are defining a public function (gleam functions are private
by default). The function has to be named main
in order for the runtime to
pick it up.
Let’s also take a gander at that auto-generated hello_world_test.gleam
file:
// hello_world_test.gleam
import gleeunit
import gleeunit/should
pub fn main() {
gleeunit.main()
}
// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
}
I like it when a language puts testing front and centre. Encoding it in the
template is a good start. Again we see a couple imports. There is a main
function, which acts as the entry-point for the file, but all it does is farm
the real work out to gleeunit
.
On Standard Libraries
Here’s an interesting thing: what is gleeunit and where is its home? You might guess, as I did, that gleeunit ships with Gleam, but you’d be wrong as part of the standard library. In fact, the gleeunit repository is not even maintained by the official Gleam organization (but the maintainer, Louis Pilfold, is the inventor of Gleam, so what difference does it make?)
Turns out, gleeunit was available to our program only because it is specified
in the gleam.toml
file:
name = "hello_world"
version = "0.1.0"
# Fill out these fields if you intend to generate HTML documentation or publish
# your project to the Hex package manager.
#
# licences = ["Apache-2.0"]
# description = "A Gleam library..."
# repository = { type = "github", user = "username", repo = "project" }
# links = [{ title = "Website", href = "https://gleam.run" }]
[dependencies]
gleam_stdlib = "~> 0.20"
[dev-dependencies]
gleeunit = "~> 0.6"
This file clearly looks like somebody thought Rust’s Cargo configuration was a good idea (it is) and decided to copy it.
Note that not only is gleeunit
specified as a dev-dependency, but even
gleam_stdlib
is a dependency. So unlike virtually other programming lanuage
I’ve ever seen, Gleam doesn’t ship with its own standard library. Seriously.
Comment out the gleam_tdlib
and gleeunit
lines by adding a #
in front of
them and run gleam run
. Check out that error:
❯ gleam run
Compiling hello_world
error: Unknown module
┌─ ./src/hello_world.gleam:1:8
│
1 │ import gleam/io
│ ^^^^^^^^
No module has been found with the name `gleam/io`.
When you think about it, it kind of makes sense. The Gleam standard library that is used when you build for Erlang is probably going to be totally different from the one that ships with Javascript. Unless you want to bundle an entire Erlang runtime with your Javascript code, and why would you ship a virtual machine when the browser already provides you a perfectly serviceable virtual machine.
(Please ignore how I’m carefully not ranting about the fact that modern browsers feel a need to ship two virtual machines. It’s not important.)
(BTW, Isn’t it amazing what you can sneak into a parenthetical?)
Two Ways To Call Functions
Gleam is a functional programming language, which means almost all you’ll be doing is passing values into functions. There’s no objects or methods or anything; just (immutable) data and the functions that operate on them.
We’ve seen three functions so far, two of them named main
, and another named
hello_world_test
. We’ve also seen three separate lines of code that call functions:
gleeunit.main()
io.println("Hello from hello_world!")
and
1
|> should.equal(1)
The first one is pretty straightforward and looks like most common languages. A module name is specified followed by a function in that module, and empty parantheses to indicate that there are no arguments.
The second one may also look pretty familiar; it’s the same general idea, except there’s a single argument inside the function.
You might be used tho the third if you know other functional languages, but I’d personally never seen anything like it before I started working with Rescript.
|>
is called the “pipe operator”, and it behaves similarly to the |
unix
pipe symbol used to connect outputs to inputs in terminal shells. I always read
it as “out to” in my head.
So in the example code, the 1
value goes “out to” the should.equal
function.
But what does it mean for a value to go out to?
Turns out it’s not terribly complicated. |>
means “insert this as the first
argument”, so if “out to” doesn’t work for you, you can always think “becomes
the first argument of”.
Two examples should eliminate any confusion:
io.println("Hello from hello_world!")
"Hello from hello_world!" |> io.println
The two lines above do exactly the same thing. They pass one string argument
into the io.println
function. The following two lines are also equivalent.
They pass two integer arguments into the should.equal
function:
1 |> should.equal(1)
should.equal(1, 1)
You’re probably asking yourself why you need two syntaxes, or indeed, which one you should choose when. The first question is answered by an example from the Gleam documentation:
string
|> string_builder.new
|> string_builder.reverse
|> string_builder.to_string
is rather easier to read than
string_builder.to_string(iodata.reverse(iodata.new(string)))
The second question is more nuanced. Programming is an art, and the decision as to whether to use pipe-first or call syntax is up to the poet. Generally, it depends on the symmetries in the particular piece of code you are writing.
My general guidance is that if you can use two or more pipe operators, you
should use two or more pipe operators. But if your function would only have one
pipe operator, it depends on how much importance you want to give each
argument. In the should.equal
case, the value being checked is of more
interest than the expected value it is being compared to (not to mention that
1 |> should.equal(1)
reads from left to right like standard English).
In other words, just be cautious of what you sneak into parentheticals.
Let’s Write Some Code
Gotta love it when a programming article is approaching two thousand words and you haven’t done any programming yet, eh? At least you know two ways to call functions already!
“Hello world” is always your first app, and “Hello
So let’s add a variable to our program and practice those two ways to call functions:
import gleam/io
import gleam/string
pub fn main() {
let name = "Traveller"
string.append("Hello, ", name)
|> io.println
}
Run this with gleam run
and you should see a nice greeting. If you haven’t
travelled much lately (who has? 🙁) you might want to change the value of the
name
variable.
Well, I say variable, which just shows how used to imperative programming I am.
In fact, all values in Gleam are immutable. They never change. So there’s
nothing variable about it. The correct term is “binding”. We are binding the
value "Traveller"
to the name name
.
We then call the string.append
function, passing it two arguments: the string
"Hello, "
and the value of name
. This function returns a new string,
which happens to be "Hello, Traveller"
. Then we pipe that new string
into the io.println
function we are already familiar with.
Note that this could also be written as:
import gleam/io
import gleam/string
pub fn main() {
let name = "Dusty"
"Hello, "
|> string.append(name)
|> io.println
}
but it looks weird to me because it gives me the sense that "Hello, "
is more
important than name
, when really it’s the result of concatenating them that is
interesting.
We could also have written it this way:
import gleam/io
import gleam/string
pub fn main() {
let name = "Dusty"
io.println(string.append("Hello, ", name))
}
which looks familiar to me as a coder of many other languages. It gives a sense
that the println
function is more important than the appended "Hello, Traveller"
string, though, which doesn’t feel quite right.
More importantly, this variation didn’t allow me to demonstrate the use of pipes and call syntax at the same time. Pedagogy wins again.
tl;dr write whichever syntax looks nicest to you.
A Brief Note on Strings
Like any modern language, strings in Gleam are UTF-8 encoded binaries and can contain any valid unicode. Try this:
import gleam/io
import gleam/string
pub fn main() {
let name = "⛄"
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
In addition to a couple unicode characters, this snippet introduces us to
lists. We made a list of three strings, piped them into the string.concat
function, and piped the result into the io.println
. (In this case, I
preferred pipes because the list of strings deserves a prominent place at the
front of the line).
Unlike most imperative languages, Gleam lists are linked lists rather than array lists or vectors. This is common in functional languages, and you’ll find that you use them a little differently. I’ll probably go into more detail in a later article.
Read The Name from Standard Input
It wouldn’t be very convenient for users of our program to have to edit it with their own name and rebuild it. Let’s instead read the value from standard input.
Surprisingly, there is no entry in the Gleam standard library for reading a
line from standard input. It’s possible this is just an oversight, but I
suspect it’s because the gleam maintainers are trying to keep the standard
library language agnostic. An io.read
command that depends on Erlang would
not be portable to Javascript (or native code if they build a native compiler).
I foresee multiple “standard” libraries for different targets. I hope this is
the case, as transpiled systems often try to ship entire runtimes on the target
platform instead of treating them as native first-class citizens.
We’re currently targeting erlang, so, we’ll have to add the gleam_erlang library. This is easily done, and you can probably guess the command (hint: see italics).
gleam add gleam_erlang
If you open the gleam.toml
file, you should see the new library listed under
dependencies.
gleam_erlang
is actually pretty bare-bones and seems like a rather random
collection of primitives. This is indicative of a very young ecosystem more
than anything. But it contains a get_line
function, which is all we need
(for now).
Change your helo_world.gleam
file as follows:
import gleam/io
import gleam/string
import gleam/erlang
pub fn main() {
assert Ok(name) = erlang.get_line("Enter your name: ")
["Hello, ", name |> string.trim, "! 🎉"]
|> string.concat
|> io.println
}
Again, we see a few new constructs. The most interesting is the assert
keyword. This construct behaves differently from what I’ve seen in most
languages. Syntactically, it acts like a suped-up let
binding.
erlang.get_line
returns a Result
type, which is used to indicate a potential error condition.
I don’t want to go into the nuances of pattern matching just yet (It’s no
different from Rust if you are familiar with Rust), so suffice it to say that
get_line
can return either Ok(value)
or Error(reason)
.
We could handle the Error
condition using a case expression (and we will, but
later). In this case, it’s really quite unlikely for get_line
to error, and I
wouldn’t know what to do with it if it did. Probably just print an error
message.
This is where assert
comes in. assert
basically says, “if the function returns
an Ok
value, bind name
to the value inside the Ok
pattern. Otherwise, crash.”
It’s a concise way to crash the program if you don’t feel like handling stuff.
It’s similar to unwrap
in Rust.
get_line
returns a string with a newline appended, so I piped the return value
into string.trim
before storing it in the list. The pipeline for concatenating
and outputting the string hasn’t changed.
Try it! gleam run
and enter your name at the prompt. Mine looks like this:
❯ gleam run
Compiling hello_world
Compiled in 0.34s
Running hello_world.main
Enter your name: Dusty
Hello, Dusty! 🎉
If you want to test out the error condition, run it and press Ctrl+D
at the
prompt to insert and end of file character. Gleam won’t like that and you’ll
get a <<"Assertion pattern match failed">>
error.
On Finding Dependencies
I’d like to digress a bit on the noble art of finding dependency libraries for Gleam. It’s not that well documented (which is one of the reasons I’m writing this), yet. There are links to the language tour, and standard library but not to other key packages in the ecosystem.
It actually took some dedicated searching to find the get_line
function,
among others. So here are a couple tips:
- Awesome Gleam contains a list of gleam libraries that may be of value to you.
- I’ve noticed that projects rarely link to their own documentation. For example, Awesome Gleam links to the gleam-erlang repository, but there isn’t much in the way of documentation there.
- hexdocs allows you to search for documentation of
various projects.
hex
is an Erlang/Elixer package manager, and most of the gleam libraries use it as well. Search forgleam
to find several packages (many of which are not listed on Awesome Gleam) and to find documentation for packages you know about.
Pattern Matching with Case Expressions
Like Rust, Rescript, and most functional languages, Gleam programs make
extensive use of pattern matching. The principle construct is the case
expression.
“Pattern matching” means that the decision as to which arm to execute depends
on the shape of the data, rather than its value. The Result
type we’ve already
seen is an extremely common “alternative values” pattern. The assert
statement
and its sibling try
help keep pattern matching on Result
tidy when you don’t
care about intermediate values, but when you want to explicitly handle the error
condition, case
is the way to do it.
Let’s check it out:
import gleam/io
import gleam/string
import gleam/erlang
pub fn main() {
case erlang.get_line("Enter your name: ") {
Error(erlang.Eof) -> io.println("\nEnd of File")
Ok(name) ->
[
"Hello, ",
name
|> string.trim,
"! 🎉",
]
|> string.concat
|> io.println
}
}
The case
keyword kicks off pattern matching and is immediately followed by the
pattern being matched on. In this case, it’s the result of get_line
.
The get_line
documentation indicates that the error reason will be an instance
of GetLineError
which is either Eof
or NoData
(I’m ignoring NoData
for now).
Curly braces indicate an expression block in Gleam. Inside the curly braces, we
have two pattern “arms”, each denoted by a pattern, followed by a ->
, and the
expression to be executed if that pattern matches. At most one of these arms
will be executed, depending on whether you type ctrl-D
or a legitimate string
at the prompt.
Most soundly typed languages let you know if your case
structure is missing
an arm, and I was disappointed to discover Gleam does not do so (at least, not
yet). It is too easy to accidentally (or intentionally, in this case) overlook
possible cases, as I did with NoData
.
This missing feature means that errors that can be caught at compile-time are actually not found until runtime, meaning the user has to learn about it for you.
I don’t know how to cause NoData
, so to demonstrate this, I changed
erlang.Eof
to erlang.NoData
in the above program. That meant that
erlang.Eof
is not properly handled. Pressing ctrl-D
at the prompt results
in an inelegant runtime error:
❯ gleam run
Compiling hello_world
Compiled in 0.32s
Running hello_world.main
Enter your name: exception error: no case clause matching {error,eof}
in function hello_world:main/0 (build/dev/erlang/hello_world/build/hello_world.erl, line 8)
Command Line Arguments
Let’s make another adjustment. Instead of querying the user on standard input, let’s give them the option to pass a value in using a command line argument.
import gleam/io
import gleam/string
import gleam/erlang
import gleam/list
pub fn main() {
let arguments = erlang.start_arguments()
assert Ok(name) =
arguments
|> list.at(0)
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
Now you can run your code with something like gleam run Dusty
you’ll see the
welcome greeting printed to the screen. I quite like my name, but if you have a
different one, then you should probbaly use that instead.
There’s a new import for the gleam/list
module, which ships with the standard
library and contains a bunch of useful functions for manipulating linked lists.
Note that we’ve still got the gleam/erlang
import even though we’ve stopped
reading from stdin for now. That’s because of start_arguments
.
The erlang.start_arguments()
function returns the command line
arguments that were passed into the program. Unlike some languages, it does not
include the name of the program itself. The return value is a list of strings.
We use the list.at
function to get the first element out of the list. Remember
how assert
works: We’re telling Gleam we want it to crash if the user doesn’t
specify at least one argument. That’s not terribly friendly, but we’re not here
to make friends, now, are we?
We’re also droppping any extra arguments the user may have supplied, meaning we’ll also alienate any friends who have multiple names. At least it won’t crash, but it will discard anything after the first argument.
Actually, that’s a lot of potentially unhappy people. Gleam advertises itself as a “friendly” language, so perhaps we should maintain that spirit.
Shape of a List
Remember how the case statement is designed to pattern match on the shape of
its argument? If the argument is a list, the “shape” is the number of elements
in the list. Let’s replace our assert
with a case
statement:
import gleam/io
import gleam/string
import gleam/erlang
import gleam/list
pub fn main() {
let arguments = erlang.start_arguments()
let name = case arguments {
[] -> "Stranger"
[name] -> name
_names -> "Big named person"
}
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
Replacing assert
with a case
is probably a common activity. You prototype
your code with assert
, then once the happy bath is working, you switch it to
case
to fix the error conditions.
This time, we’ve replaced the assert
with three possible arms. If the user
has not supplied a name, we call them “Stranger”. If they give us exactly one
argument, that’s what we call them. But if they give us many arguments, we just
give them a stupid nickname. The “shape” of the list chooses which of those paths
is taken.
Note: The _
in _name
means “ignore this value, I’m not using it”.
Truth be told, neither nicknames nor the title “stranger” is particularly friendly, but our program is definitely warming up to folks a bit. Let’s see if we can draw it further out of its shell.
Concatenating with Spaces
Let’s tackle the nickname scenario first. Basically, if we are given a large
list, we want to concatenate the arguments in the list. Your first attempt
might be to replace the _names
arm with the following:
names ->
names
|> string.concat
I’ve removed the underscore now because we are actually using the variable.
However, this naive attempt makes our program look like it’s talking really fast:
❯ gleam run -- Dusty Phillips
Compiling hello_world
Compiled in 0.40s
Running hello_world.main
Hello, DustyPhillips! 🎉
I went to all that trouble to type a space in the argument and my program took it upon itself to just remove it. How rude!
In Python, I’d write " ".join(names)
which is an admittedly bizarre construct.
Gleam has a join
function in the string
library, and with pipe-first syntax,
it’s much more elegante: names |> string.join(" ")
import gleam/io
import gleam/string
import gleam/erlang
import gleam/list
pub fn main() {
let arguments = erlang.start_arguments()
let name = case arguments {
[] -> "Stranger"
[name] -> name
names ->
names
|> string.join(" ")
}
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
Now if I run the program with a name my mother tended to call me when I was young, it reacts properly (even if I didn’t):
❯ gleam run -- Dustin Dont Bug The Dog When Its Eating
Compiling hello_world
Compiled in 0.33s
Running hello_world.main
Hello, Dustin Dont Bug The Dog When Its Eating! 🎉
Apropos of nothing, I have bite scars to prove I didn’t like that name. I’m told it was “my own damn fault”.
Interactive Fallback
Our program is pretty friendly now, if you consider calling someone “Stranger”
to be friendly (some subcultures do, some don’t). It might be more polite to
invite them to introduce themselves, though. To do that, we can nest our
original get_line
case statement within the new argument
based one, prompting
for a name only if they don’t give us one:
import gleam/io
import gleam/string
import gleam/erlang
import gleam/list
pub fn main() {
let arguments = erlang.start_arguments()
let name = case arguments {
[] ->
case erlang.get_line("Howdy Stranger, what is your name? ") {
Ok(name) ->
name
|> string.trim
Error(_) -> "Stranger"
}
[name] -> name
names ->
names
|> list.intersperse(" ")
|> string.concat
}
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
But that case looks rather messy.
Tidying Up
In addition to assert
(and try
, which I’m not going to cover today), the
gleam.result module in the
standard library has a bunch of nice functions to make working with results feel
cleaner.
Result
s are typically the return value of any function that might fail, and
in my experience, it is not uncommon to have deep chains of them. Thus it is
very important that working with them be ergonomic for the author of code, and
highly legible for the reader.
The “Give me a value if it’s Ok, otherwise use this default” case is a
very common action to take when given a Result value. The gleam standard library
gives us the result.unwrap
function, which makes our code much easier on the eyes:
import gleam/io
import gleam/string
import gleam/erlang
import gleam/list
pub fn main() {
let arguments = erlang.start_arguments()
let name = case arguments {
[] ->
erlang.get_line("Howdy Stranger, what is your name? ")
|> result.unwrap("Stranger")
names ->
names
|> list.intersperse(" ")
|> string.concat
}
["Hello, ", name, "! 🎉"]
|> string.concat
|> io.println
}
I’ve also removed the single element name
arm because the multi-element arm
handled it just as well.
Summary
That’s all I’ve got for today. Gleam is a pretty language that I can tell I’m going to have a lot of fun with. It’s immature, but it is building on a solid foundation that I think will grow into a very ergonomic coding experience.
This article covered the “hello world” of Gleam development, and introduced some iconic functional paradigms including pipes and pattern matching. We encountered a couple of standard library functions in string and list, and also learned that some functionality that we might consider standard actually come from a separate library.
Most importantly, we developed a friendly program. I hope you enjoyed the writing and will return for the next instalment. Don’t be a stranger!