Beginner

Variables and Types in Gleam

Learn about let bindings, basic types, custom types, and type inference in Gleam with practical Docker-ready examples

Gleam is a statically typed functional language where every binding is immutable and the compiler infers types automatically. Unlike many languages, Gleam draws a hard line between integers and floats — they have entirely separate operators and cannot be mixed without explicit conversion. This strict separation, combined with custom algebraic data types and exhaustive pattern matching, means the compiler catches a wide range of bugs before your code ever runs.

As a functional language, Gleam has no mutable variables. Instead, you create immutable let bindings. If you need a new value, you create a new binding — you can even reuse the same name through shadowing, which creates a fresh binding that replaces the previous one in scope.

In this tutorial, you’ll explore Gleam’s basic types, see how Int and Float stay strictly separate, work with type annotations and conversions, and build your own custom types with variants and generics.

Let Bindings and Basic Types

Gleam has five basic types: String, Int, Float, Bool, and Nil. Every value is created with a let binding, and the compiler infers the type from the value on the right-hand side.

Create a file named variables.gleam:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import gleam/float
import gleam/int
import gleam/io

pub fn main() {
  // Basic types - the compiler infers each type
  let name = "Gleam"
  let year = 2016
  let version = 1.14
  let is_functional = True

  io.println("=== Basic Types ===")
  io.println("String: " <> name)
  io.println("Int: " <> int.to_string(year))
  io.println("Float: " <> float.to_string(version))
  io.println("Bool: " <> bool_to_string(is_functional))

  // Type annotations are optional but serve as documentation
  let language: String = "Gleam"
  let major: Int = 1
  let rating: Float = 9.5

  io.println("")
  io.println("=== Type Annotations ===")
  io.println(language <> " v" <> int.to_string(major))
  io.println("Rating: " <> float.to_string(rating))

  // Int and Float use SEPARATE operators: + vs +.  * vs *.
  let int_sum = 10 + 25
  let float_sum = 1.5 +. 2.5

  io.println("")
  io.println("=== Separate Numeric Types ===")
  io.println("Int: 10 + 25 = " <> int.to_string(int_sum))
  io.println("Float: 1.5 +. 2.5 = " <> float.to_string(float_sum))

  // Explicit type conversions between Int and Float
  let x = 42
  let x_as_float = int.to_float(x)
  let y = 3.75
  let y_rounded = float.round(y)
  let y_truncated = float.truncate(y)

  io.println("")
  io.println("=== Type Conversions ===")
  io.println(
    "Int to Float: " <> int.to_string(x) <> " -> " <> float.to_string(
      x_as_float,
    ),
  )
  io.println(
    "Float rounded: "
    <> float.to_string(y)
    <> " -> "
    <> int.to_string(y_rounded),
  )
  io.println(
    "Float truncated: "
    <> float.to_string(y)
    <> " -> "
    <> int.to_string(y_truncated),
  )

  // Shadowing: rebind a name to a new value
  let count = 1
  io.println("")
  io.println("=== Shadowing ===")
  io.println("count = " <> int.to_string(count))
  let count = count + 1
  io.println("count (rebound) = " <> int.to_string(count))
}

fn bool_to_string(value: Bool) -> String {
  case value {
    True -> "True"
    False -> "False"
  }
}

A few things stand out here. Gleam has no toString method on values — you use module functions like int.to_string and float.to_string for explicit conversion to strings. The <> operator concatenates strings. And since there’s no built-in bool.to_string, we wrote a small helper using pattern matching — a natural Gleam pattern.

The strict separation between Int and Float is intentional. You cannot write 1 + 2.5 — the compiler will reject it. Integer operators (+, -, *, /) and float operators (+., -., *., /.) are distinct, forcing you to be explicit about which numeric type you’re working with. This prevents subtle precision bugs that plague languages with implicit numeric coercion.

Custom Types, Generics, and Collections

Gleam’s custom types are algebraic data types — they can have multiple variants, each carrying different data. Combined with exhaustive pattern matching, the compiler ensures you handle every possible case. Types can also be generic, accepting type parameters that make them reusable across different data.

Create a file named variables_custom.gleam:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
import gleam/float
import gleam/int
import gleam/io

// Custom type with multiple variants (algebraic data type)
type Color {
  Red
  Green
  Blue
  Custom(r: Int, g: Int, b: Int)
}

// Single-variant type acts like a record/struct
type Point {
  Point(x: Float, y: Float)
}

// Generic type - works with any types for first and second
type Pair(a, b) {
  Pair(first: a, second: b)
}

pub fn main() {
  // Custom types with variants
  let color = Blue
  let custom_color = Custom(r: 255, g: 128, b: 0)

  io.println("=== Custom Types ===")
  io.println("Color: " <> color_to_string(color))
  io.println("Custom: " <> color_to_string(custom_color))

  // Single-variant types support field access with dot notation
  let origin = Point(x: 0.0, y: 0.0)
  let p = Point(x: 3.0, y: 4.0)

  io.println("")
  io.println("=== Record-Style Types ===")
  io.println("Origin: " <> point_to_string(origin))
  io.println("Point: " <> point_to_string(p))

  // Record update syntax creates a new value with some fields changed
  let moved = Point(..p, y: 10.0)
  io.println("Moved: " <> point_to_string(moved))

  // Generic types infer their type parameters from usage
  let pair = Pair(first: "age", second: 30)

  io.println("")
  io.println("=== Generic Types ===")
  io.println(
    "Pair: (" <> pair.first <> ", " <> int.to_string(pair.second) <> ")",
  )

  // Tuples - anonymous groupings of values
  let coords = #(10, 20)
  let #(cx, cy) = coords

  io.println("")
  io.println("=== Tuples ===")
  io.println("x=" <> int.to_string(cx) <> ", y=" <> int.to_string(cy))

  // Lists - linked lists where all elements share the same type
  let numbers = [1, 2, 3, 4, 5]
  let with_zero = [0, ..numbers]
  let first = case with_zero {
    [head, ..] -> int.to_string(head)
    [] -> "empty"
  }

  io.println("")
  io.println("=== Lists ===")
  io.println("First element: " <> first)
  io.println(
    "List length: " <> int.to_string(list_length(with_zero, 0)),
  )
}

fn color_to_string(color: Color) -> String {
  case color {
    Red -> "Red"
    Green -> "Green"
    Blue -> "Blue"
    Custom(r, g, b) ->
      "RGB("
      <> int.to_string(r)
      <> ", "
      <> int.to_string(g)
      <> ", "
      <> int.to_string(b)
      <> ")"
  }
}

fn point_to_string(p: Point) -> String {
  "(" <> float.to_string(p.x) <> ", " <> float.to_string(p.y) <> ")"
}

fn list_length(items: List(a), acc: Int) -> Int {
  case items {
    [] -> acc
    [_, ..rest] -> list_length(rest, acc + 1)
  }
}

Custom types are central to Gleam programming. The Color type shows how variants can range from simple labels (Red, Green, Blue) to variants carrying data (Custom with RGB values). The Point type demonstrates single-variant types that act like records — you get field access with dot notation and update syntax with .. to create modified copies.

The Pair type introduces generics: Pair(a, b) works with any types, and the compiler infers the concrete types from how you use it. Tuples serve a similar purpose for quick, anonymous groupings without defining a named type.

Lists in Gleam are singly-linked and immutable. Prepending with [0, ..numbers] is efficient because it reuses the existing list. Pattern matching on lists with [head, ..rest] is the idiomatic way to destructure them, and the recursive list_length function shows how functional iteration replaces loops.

Running with Docker

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

# Run the basic types example
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/variables.gleam hello/src/hello.gleam && cd hello && gleam run'

# Run the custom types example
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/variables_custom.gleam hello/src/hello.gleam && cd hello && gleam run'

Expected Output

Output from variables.gleam:

=== Basic Types ===
String: Gleam
Int: 2016
Float: 1.14
Bool: True

=== Type Annotations ===
Gleam v1
Rating: 9.5

=== Separate Numeric Types ===
Int: 10 + 25 = 35
Float: 1.5 +. 2.5 = 4.0

=== Type Conversions ===
Int to Float: 42 -> 42.0
Float rounded: 3.75 -> 4
Float truncated: 3.75 -> 3

=== Shadowing ===
count = 1
count (rebound) = 2

Output from variables_custom.gleam:

=== Custom Types ===
Color: Blue
Custom: RGB(255, 128, 0)

=== Record-Style Types ===
Origin: (0.0, 0.0)
Point: (3.0, 4.0)
Moved: (3.0, 10.0)

=== Generic Types ===
Pair: (age, 30)

=== Tuples ===
x=10, y=20

=== Lists ===
First element: 0
List length: 6

Key Concepts

  • All bindings are immutable — there are no mutable variables in Gleam. Use shadowing (rebinding the same name) when you need an updated value.
  • Type inference handles most cases — the compiler deduces types from values, so annotations are optional but useful as documentation.
  • Int and Float are strictly separate — they use different operators (+ vs +.) and require explicit conversion functions like int.to_float and float.round.
  • Custom types are algebraic data types — they can have multiple variants, each carrying different data, and the compiler forces exhaustive pattern matching over all variants.
  • Single-variant types work like records — they support dot notation for field access and .. syntax for creating modified copies.
  • Generic types use type parametersPair(a, b) works with any types, inferred automatically from usage.
  • Lists are singly-linked and immutable — prepending is efficient, and pattern matching with [head, ..rest] is the standard way to process them.
  • No null, no exceptions — Gleam uses Option for missing values and Result for operations that can fail, keeping the type system honest.

Running Today

All examples can be run using Docker:

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

Comments

Loading comments...

Leave a Comment

2000 characters remaining