Gleam Basics and Tooling
Learning Objectives
- You can read basic Gleam syntax and know the basic keywords
fn
,const
,let
andimport
- You can work with strings in Gleam and know how integers and floating point numbers work in Gleam
- You know how to define your own functions in Gleam
Warming up
Before starting to look into Gleam, let’s first warm up a bit. Below, you’ll find a sequence of short exercises on Gleam.
Gleam Basics
Gleam is a functional (as opposed to imperative) statically typed (as opposed to dynamically typed) programming language. Gleam, like many functional programming languages, has a small syntax with only a dozen or so keywords. The language, on first sight, looks very much like Rust, which as been one of the inspirations for Gleam.
Gleam uses much of the same keywords as Rust, but the similarities don’t stop there. In a later part that focuses on the Rust programming language, we make more comparisons between Rust and Gleam.
A rhetorical question — which programming language tutorial starts with anything else except print
? In the Haskell programming language you start with foundations like functions and types — “hello, world” comes much later.
Gleam is unlike Haskell in this aspect as it is not a pure functional programming language, so maybe we can start with a “Hello, World!” program.
A Hello World program in Gleam looks like the following. You can run the program by pressing the play icon (). When you press the play icon, the program is compiled and run, and the output is displayed below the code editor — the output includes also information on how long the compilation took.
A Gleam program starts by executing the main
function on startup. A function has a visibility modifier pub
which means it is public and can be called from other modules and a fn
keyword which denotes a function, followed by the name of the function and parentheses ()
. The function body is enclosed in curly braces {}
.
The io.println
function is used to print a string to the standard output — to use it, we need to import it from the gleam/io
module.
Gleam and Dart
Here is a piece of code written in Gleam. Like before, you can run it by pressing the play icon. The function sum
calculates the sum of a list of integers, and the main
function calls the function, printing the result of the function call.
The same code in Dart would be written as follows.
The notable differences in the syntax of these languages are as follows:
Only in Gleam | In both | Only in Dart |
---|---|---|
|
|
|
For someone already familiar with Dart and not familiar with Gleam, the syntax of Gleam may seem a bit strange at first. Similarly, Dart might be easier to understand for someone who is already familiar with other programming languages like Scala.
Let’s start building familiarity with Gleam by looking at some basic features of the language.
Variables
Variables are created using the let
keyword, which is followed by the name of the variable and an expression that evaluates to the value of the variable.
let x = 5
let this_is_x = x
let x2 = 7
In the above example, we create three variables x
, this_is_x
and x2
. The variable x
is assigned the value 5
, this_is_x
is assigned the value of x
, and x2
is assigned the value 7
.
Gleam follows the convention of using snake_case for variable names. Variable names and identifiers can contain any lowercase alphanumeric characters
a-z
,0-9
and underscores_
, but must not start with a number.
As Gleam is a functional language, there is no way to mutate variables. Variables act as constants, but calling them constants would be confusing1. To change the value in a variable, we need to define a new variable with the new value — this is called shadowing. Shadowing is the process of creating a new variable with the same name as an existing variable, which effectively hides the existing variable.
In the above example, a new variable named x
is created on each line with a new let
binding. Shadowing has the effect that evaluating variables looks at the closest binding of said variable.
The above code could be also viewed as follows, with explicit code blocks We can see this structure more clearly by adding explicit code blocks like so:
Now on line (a)
Gleam will use the variable from the let-binding (a)
when evaluating the expression x + 1
resulting in the value 6
.
On line (c)
the expression x + 1
uses x
the nearest binding2 (b)
.
And finally at (d)
the variable refers to the binding (c)
where the expression evaluated to 7.
Gleam is block-scoped, which means that variables are only available within the block they are defined in. Although the above example prints 7
, the value of the variable x
from the outer scope is still 5
.
Integer Arithmetic
Gleam provides the usual integer operations:
Notable things to note about integers in Gleam:
5 / 2
=2
remains integral.- As usual, the modulo operator
%
works like in most programming languages along with any potential pitfalls.You might want to consider using
int.modulo
(floored) instead of%
if you are dealing with negative integers. - There’s no power operator or division that results in a floating point number.
- Regular parentheses aren’t used for grouping arithmetic expressions, instead curly braces are used.
Strings
String literals are written using double quotes ("
) and can span over multiple lines.
let s = "This is the first line
This is the second"
They can include arbitrary UTF-8 characters (e.g. emoticons) and special characters can be escaped using a backslash \
.
String literals are of type String
.
Here are some common operations you can do with String
s:
- Concatenation with
<>
. - Reversing them with
string.reverse
. - Splitting at a substring with
string.split
. - Joining a list of strings with a delimiter
string.join
.
Notice however, that there is no string interpolation.
Concatenation only works for strings. To concatenate an integer to a string, you need to convert the integer to a string first — this is done with the int.to_string
function that is imported from the gleam/int
module.
Case expressions and control flow
Gleam does not have an if
or a switch
statement — it uses case expressions for all control flow.
A case expression evaluates a given expression and returns a value based on the evaluation result. The return options are given as a set of tuples, where the first element is a pattern to match against the expression, and the second element is the value to return if the pattern matches. The tuples are separated by ->
and the default pattern is denoted by _
.
The following function describe_number
takes an integer n
and returns a string describing the number.
pub fn describe_number(n) {
case n {
0 -> "Zero"
1 -> "One"
_ -> "Another number"
}
}
The expression given to the case
expression is evaluated and matched against the patterns in the tuples. If the expression matches a pattern, the value of the tuple is returned. To create an if - else
-like construct, we can use a case
expression with a single pattern.
The following code snippet demonstrates the use of a case
expression to determine if a number is positive.
import gleam/io
pub fn main() {
let x = 5
let is_positive = case x > 0 {
True -> "Yes"
False -> "No"
}
io.debug(is_positive)
}
Case expressions can also be nested. The following code snippet demonstrates a classic if - else if - else structure with a nested case
expression.
import gleam/io
pub fn main() {
let x = 5
let description = case x > 5 {
True -> "Greater than five"
False -> case x < 0 {
True -> "Smaller than 0"
False -> "Between 0 and 5"
}
}
io.debug(description)
}
Functions
We have already seen functions in action, but let’s now actually focus on what kind of objects functions are in Gleam. We start by looking at “untyped”3 numerical functions from which we pivot to discussing types and function signatures.
To define a new function in Gleam, the fn
keyword needs to be entered followed by the name of the function. The name can be any valid identifier, which means it must not contain uppercase letters. Function names should use snake_case — there’s isn’t really any other option.
A function five
returning the integer value 5
can be written as follows. When you try to run the following code, you’ll notice that there’s an error as there is no main
function defined.
The function’s definition is written using a block, which consists of curly braces with multiple expressions inside.
In Gleam, there is no return
keyword, instead the value that the block evaluates to is “returned”.
To add parameters to a function, we write them inside the parentheses after the name of the function.
The following function takes three parameters a
, b
and c
.
Function Overloading
Two functions with the same name must not be defined in the same file.
For example defining the function function
twice, first as a nullary (i.e. taking no arguments) and then as a unary (i.e. taking a single argument) function, is not allowed as Gleam doesn’t have function overloading.
However, if we import println
using import gleam/io.{println}
making it available in the file’s namespace, we can shadow it with a new function println
.
Run the following code to see this in action:
Notice the warning?
See what happens if you change print
to a println
instead.
Pipe Operator
Gleam has an operator, the pipe |>
, which is widely used to call functions in a streamlined concatenative style.
For example, the following code:
Can be written as follows using the pipe operator:
This makes the code look more natural with the pipeline consisting of “method calls”.
Data Collections
Tuples and Lists
In Gleam, tuples (tuple types) are created with the syntax #(a, b, ...)
where a
and b
are values (types) respectively.
We can access the th element of the tuple with a dot syntax .n
.
Lists use square bracket syntax for constructing a list.
The type of a list of integers is List(Int)
.
To access the first element of a list or the last element of a list, we can use the first
and last
functions from the gleam/list
module. The functions return a result, which then needs to be handled using a case
expression.
In the following example, the first element of the list is accessed using the first
function.
Lists are covered much more deeply in a later chapter.
Dictionaries
There is no built-in syntax for key-value dictionaries, but a list of tuples can be converted into one using dict.from_list
.
What Is Missing in Gleam
While Gleam is a powerful and expressive language, it currently lacks certain features commonly found in other languages:
- Object-Oriented Programming (OOP) Features: Gleam does not support traditional OOP concepts like classes and inheritance.
- Macros: Gleam does not have a macro system for metaprogramming.
- Type Classes: Unlike Haskell, Gleam does not support type classes for ad-hoc polymorphism.
- Early Returns: Gleam emphasizes expression-oriented programming, so early returns are handled differently.
- Null Values: Instead of
null
orNone
, Gleam uses theOption
type to represent values that may or may not be present, promoting safer code by enforcing handling of potential absence.
Tooling
Installing Gleam on Your Machine
For the purposes of this course, it is not strictly necessary to install a Gleam development environment on ones computer, as the programming exercises can be completed via the interactive code editors on this site. However, it is still highly recommended.
The Gleam language is compiled for the Erlang runtime environment, where it is run. Therefore, in order to run Gleam locally, one also needs to install Erlang. The official installing instructions cover the installation process for Gleam and Erlang.
As with Dart, we recommend using an editor such as VSCode with the Gleam extension installed. Other editors which have Gleam support are neovim with gleam.vim and GNU Emacs with gleam-mode.
Creating a new project with the Gleam CLI
Common to many modern and emerging programming languages,
Gleam has one, centralized, CLI tool for doing everything
from dependency management to project creation and running unit tests.
Running the gleam
command without any arguments gives us
a list of its capabilities:
$ gleam
gleam 1.6.2
Usage: gleam <COMMAND>
Commands:
add Add new project dependencies
build Build the project
check Type check the project
clean Clean build artifacts
deps Work with dependency packages
docs Render HTML documentation
export Export something useful from the Gleam project
fix Rewrite deprecated Gleam code
format Format source code
help Print this message or the help of the given subcommand(s)
hex Work with the Hex package manager
lsp Run the language server, to be used by editors
new Create a new project
publish Publish the project to the Hex package manager
remove Remove project dependencies
run Run the project
shell Start an Erlang shell
test Run the project tests
update Update dependency packages to their latest versions
Options:
-h, --help Print help
-V, --version Print version
We can create a new project with gleam new
$ gleam new mepl
Your Gleam project mepl has been successfully created.
The project can be compiled and tested by running these commands:
cd mepl
gleam test
If we cd
to the new project directory, we can
see that the gleam
CLI has generated the following project structure:
$ tree
.
├── gleam.toml
├── README.md
├── src
│ └── mepl.gleam
└── test
└── mepl_test.gleam
3 directories, 4 files
src/mepl.gleam
contains the following code:
import gleam/io
pub fn main() {
io.println("Hello from mepl!")
}
We can run it with gleam run
to confirm that everything works:
$ gleam run
Compiling gleam_stdlib
Compiling gleeunit
Compiling mepl
Compiled in 1.30s
Running mepl.main
Hello from mepl!
IDE and editor integration
Gleam has a VSCode plugin that provides syntax highlighting, code completion, and other features. You can install it from the VSCode Marketplace.
Footnotes
Footnotes
-
Calling them variables actually makes sense, because each let-binding can be thought of as a new function, as we will see in a later chapter in this course. ↩
-
Gleam doesn’t have recursive
let
bindings, which means that when reasoning aboutlet
expressions, the nearest always refers to a previouslet
expression and not the one being defined. ↩ -
The functions are not actually untyped like in dynamically typed programming languages, but instead get their type inferred from the body of the function. ↩