Gleam Records

Records group related values together under named fields. Where tuples use position to identify values, records use names. Named fields make code self-documenting — you always know what each piece of data means without counting positions.

Defining a Record

Records in Gleam are defined as custom types with a single constructor:

type Person {
  Person(name: String, age: Int, email: String)
}

Record Type Anatomy
──────────────────────────────────────────────────
type Person {
  Person(name: String, age: Int, email: String)
  │      │             │         │
  │      └── constructor name    │
  └── type name         └── field with type
}

The type name (Person) and the constructor name (Person) are the same here — this is the standard convention for single-variant types.

Creating a Record

let user = Person(
  name: "Sunita",
  age: 31,
  email: "sunita@example.com"
)

You use the field labels when creating a record. The labels make the creation call clear — no guessing which value goes in which position.

Accessing Record Fields

Use dot notation to read a field:

let name  = user.name    // "Sunita"
let age   = user.age     // 31
let email = user.email   // "sunita@example.com"

Field Access Diagram
──────────────────────────────────────────────────
user = Person("Sunita", 31, "sunita@example.com")

user.name  → "Sunita"
user.age   → 31
user.email → "sunita@example.com"

Updating a Record

Records are immutable. To "change" a field, create a new record with the updated value using the spread syntax:

let older_user = Person(..user, age: 32)
// Name and email stay the same, age becomes 32

Record Update Syntax
──────────────────────────────────────────────────
Original:  Person("Sunita", 31, "sunita@example.com")

Person(..user, age: 32)
         │         │
         │         └── override this field
         └── copy all other fields from user

Result:   Person("Sunita", 32, "sunita@example.com")

Pattern Matching on Records

Destructure a record in a case expression:

pub fn describe(person: Person) -> String {
  case person {
    Person(name: n, age: a, email: _) if a < 18 ->
      n <> " is a minor"
    Person(name: n, age: a, email: _) ->
      n <> " is " <> int.to_string(a) <> " years old"
  }
}

Records Inside Functions

type Product {
  Product(name: String, price: Float, in_stock: Bool)
}

pub fn apply_discount(p: Product, rate: Float) -> Product {
  let new_price = p.price *. (1.0 -. rate)
  Product(..p, price: new_price)
}

pub fn main() {
  let item = Product(name: "Laptop", price: 50000.0, in_stock: True)
  let sale_item = apply_discount(item, 0.10)
  // sale_item.price = 45000.0
}

Nested Records

type Address {
  Address(street: String, city: String, pincode: String)
}

type Customer {
  Customer(name: String, address: Address)
}

let customer = Customer(
  name: "Rajesh",
  address: Address(
    street: "12 Park Road",
    city: "Delhi",
    pincode: "110001"
  )
)

let city = customer.address.city  // "Delhi"

Records vs Tuples vs Maps


Comparison Table
──────────────────────────────────────────────────
Feature         │ Record  │ Tuple   │ Map
────────────────┼─────────┼─────────┼────────────
Named fields    │ Yes     │ No      │ Yes (keys)
Fixed structure │ Yes     │ Yes     │ No
Type-checked    │ Yes     │ Yes     │ Partial
Best for        │ Entities│ Pairs   │ Dynamic keys
Compile-safe    │ Yes     │ Yes     │ Key lookups
                │         │         │ return Result

Practical Example — Order System

import gleam/io

type Order {
  Order(
    id: Int,
    item: String,
    quantity: Int,
    unit_price: Float
  )
}

pub fn total(order: Order) -> Float {
  int.to_float(order.quantity) *. order.unit_price
}

pub fn confirm(order: Order) -> String {
  "Order #"
  <> int.to_string(order.id)
  <> ": "
  <> order.item
  <> " x"
  <> int.to_string(order.quantity)
  <> " = ₹"
  <> float.to_string(total(order))
}

pub fn main() {
  let o = Order(id: 1001, item: "Notebook", quantity: 5, unit_price: 120.0)
  io.println(confirm(o))
  // Order #1001: Notebook x5 = ₹600.0
}

Key Points


Record Essentials
──────────────────────────────────────────────────
1. Define with: type Name { Name(field: Type, ...) }
2. Create with: Name(field: value, ...)
3. Access with: record.field_name
4. Update with: Name(..original, field: new_value)
5. Pattern match to destructure
6. Immutable — updates produce new records
7. Use when data has 2+ named, typed fields

Records bring structure and clarity to your data. When you see order.unit_price, you know exactly what it is — no index counting, no documentation hunting. Descriptive field names are documentation built directly into the type.

Leave a Comment

Your email address will not be published. Required fields are marked *