Beginner

Hello World in Gleam

Your first Gleam program - the classic Hello World example with Docker setup

Every programming journey starts with Hello World. Let’s write our first Gleam program.

The Code

Create a file named hello.gleam:

1
2
3
4
5
import gleam/io

pub fn main() {
  io.println("Hello, World!")
}

Three lines to print to the console - clean and readable.

Understanding the Code

  • import gleam/io - Imports the io module from the standard library for console output
  • pub fn main() - Defines the public main function, which is the program’s entry point
  • io.println() - Prints a string to standard output with a newline
  • Curly braces - Gleam uses C-family style syntax with curly braces for blocks

Running with Docker

Gleam requires a project structure to run. The easiest way to try it without installing locally is with Docker:

1
2
3
4
5
# Pull the Gleam image (includes Erlang runtime)
docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine

# Run your program (creates a project, copies your file, and runs it)
docker run --rm -v $(pwd):/work ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine sh -c "gleam new hello --skip-git > /dev/null 2>&1 && cp /work/hello.gleam hello/src/hello.gleam && cd hello && gleam run"

Why the Extra Steps?

Unlike scripting languages, Gleam uses a project-based build system (similar to Rust’s Cargo). The gleam new command creates the project structure with a gleam.toml manifest and dependency configuration. The command above automates this by:

  1. Creating a new project called hello
  2. Copying your source file into the project’s src/ directory
  3. Running gleam run to compile and execute

Running Locally

If you have Gleam installed (along with Erlang):

1
2
3
4
5
6
# Create a new project
gleam new hello
cd hello

# Replace src/hello.gleam with your code, then:
gleam run

Install Gleam via:

1
2
3
4
5
6
# macOS
brew install gleam

# Or with asdf version manager
asdf plugin add gleam
asdf install gleam latest

Note: Gleam also requires Erlang to be installed for the BEAM compilation target.

Expected Output

Hello, World!

Key Concepts

  1. Static types - Gleam checks types at compile time, catching errors before your code runs
  2. Type inference - The compiler figures out types automatically; you rarely need annotations
  3. Module system - Functions live in modules (like gleam/io), imported explicitly
  4. No semicolons - Expressions are separated by newlines
  5. BEAM VM - Compiles to Erlang bytecode, running on the battle-tested Erlang virtual machine

Project Structure

When you create a Gleam project with gleam new, you get:

hello/
├── gleam.toml        # Project manifest (name, version, dependencies)
├── src/
│   └── hello.gleam   # Your source code
└── test/
    └── hello_test.gleam  # Tests

The gleam.toml file manages dependencies from Hex, the package repository shared with Erlang and Elixir.

A More Gleam-Style Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import gleam/io
import gleam/string

pub fn main() {
  let greeting = greet("World")
  io.println(greeting)
}

fn greet(name: String) -> String {
  string.concat(["Hello, ", name, "!"])
}

This demonstrates:

  • let bindings - Immutable variable assignment
  • Private functions - fn without pub is module-private
  • Type annotations - Optional but useful for documentation
  • String concatenation - Using the string module from the standard library

Pattern Matching

Gleam’s case expression is powerful:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import gleam/io

pub fn main() {
  let greeting = greet("World")
  io.println(greeting)
}

fn greet(name: String) -> String {
  case name {
    "World" -> "Hello, World!"
    other -> "Hello, " <> other <> "!"
  }
}

The compiler ensures you handle every possible case - if you miss one, it tells you at compile time.

The Pipe Operator

Gleam’s pipe operator (|>) chains function calls, passing the result of each as the first argument to the next:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import gleam/io
import gleam/string

pub fn main() {
  "world"
  |> string.capitalise
  |> string.append("Hello, ", _)
  |> string.append("!")
  |> io.println
}

Data flows top to bottom, making transformations easy to follow.

Result Type for Error Handling

Gleam uses the Result type instead of exceptions:

1
2
3
4
5
6
7
8
9
import gleam/int
import gleam/io

pub fn main() {
  case int.parse("42") {
    Ok(n) -> io.println("Parsed: " <> int.to_string(n))
    Error(_) -> io.println("Not a number!")
  }
}

This forces you to handle both success and failure cases explicitly - no uncaught exceptions.

Common Beginner Notes

No Null Values

Gleam has no null or nil. Use the Option type for values that might not exist:

1
2
3
4
5
6
7
8
import gleam/option.{type Option, None, Some}

fn find_user(id: Int) -> Option(String) {
  case id {
    1 -> Some("Alice")
    _ -> None
  }
}

All Data Is Immutable

Every value in Gleam is immutable. “Updating” a value creates a new one:

1
2
let list = [1, 2, 3]
let new_list = [0, ..list]  // [0, 1, 2, 3] - original unchanged

Erlang and Elixir Interop

Gleam can call Erlang and Elixir functions directly using external functions, giving you access to the entire BEAM ecosystem.

Running Today

All examples can be run using Docker:

docker pull ghcr.io/gleam-lang/gleam:v1.14.0-erlang-alpine
Last updated: