Fun and Functions with Gleam

Gleam Basics and Tooling


Learning Objectives

  • You can read basic Gleam syntax and know the basic keywords fn, const, let and import
  • 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.

Loading Exercise...

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.

Run the program to see the output

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.

Loading Exercise...

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.

Run the program to see the output

The same code in Dart would be written as follows.

Run the program to see the output

The notable differences in the syntax of these languages are as follows:

Only in GleamIn bothOnly in Dart
  • pub keyword is used to denote a public item.
  • The last expression is returned from a function.
  • Types of parameters are written using a colon :. Return type is indicated with an arrow ->.
  • Method call syntax isn’t available. Instead a top-level function needs to be imported.
  • Curly braces {} are used for code structure and indendation is only visual.
  • Parameter and return types need to be provided for explicit function statements.
  • Implicit function expressions are easy to define.
  • A semicolon ; must be entered after each statement.
  • Types are written before the identifier
  • Type parameters use angle brackets <>.
  • Method call syntax list.fold is available.

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

Official documentation

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.

Run the program to see the output

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:

Run the program to see the output

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.

Loading Exercise...

Integer Arithmetic

Official documentation

Gleam provides the usual integer operations:

Run the program to see the output

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

Official documentation

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 Strings:

  • Concatenation with <>.
  • Reversing them with string.reverse.
  • Splitting at a substring with string.split.
  • Joining a list of strings with a delimiter string.join.
Run the program to see the output

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.

Run the program to see the output
Loading Exercise...

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)
}
Loading Exercise...

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.

Run the program to see the output

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.

Run the program to see the output
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.

Run the program to see the output

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:

Run the program to see the output

Notice the warning? See what happens if you change print to a println instead.

Pipe Operator

Official documentation

Gleam has an operator, the pipe |>, which is widely used to call functions in a streamlined concatenative style.

For example, the following code:

Run the program to see the output

Can be written as follows using the pipe operator:

Run the program to see the output

This makes the code look more natural with the pipeline consisting of “method calls”.

Data Collections

Tuples and Lists

Official documentation for tuples, 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.

Run the program to see the output

Lists use square bracket syntax for constructing a list. The type of a list of integers is List(Int).

Run the program to see the output

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.

Run the program to see the output

Lists are covered much more deeply in a later chapter.

Loading Exercise...

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.

Run the program to see the output

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 or None, Gleam uses the Option 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

  1. 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.

  2. Gleam doesn’t have recursive let bindings, which means that when reasoning about let expressions, the nearest always refers to a previous let expression and not the one being defined.

  3. The functions are not actually untyped like in dynamically typed programming languages, but instead get their type inferred from the body of the function.