Master Bettys Bike Shop in Gleam: Complete Learning Path


Master Bettys Bike Shop in Gleam: A Complete Learning Path

The Bettys Bike Shop module is a core part of the kodikra.com Gleam curriculum, designed to teach you robust data modeling using custom types and pattern matching. You'll learn to represent real-world concepts like inventory and operations in a type-safe, expressive, and error-resistant way.


The Frustration of Fragile Code (And How Gleam Fixes It)

Imagine you're building a simple inventory system for a local bike shop. Your first instinct might be to use basic data types. Maybe you represent a bike with a string: "tricycle" or an integer for the number of wheels: 3. It works, for a while. But what happens when someone accidentally types "trycycle" or enters 4 for the number of wheels? Your program crashes, or worse, continues with corrupt data.

This is a common pain point for developers. Relying on primitive types for complex domain logic creates "stringly-typed" or "integerly-typed" code that is brittle, hard to refactor, and prone to silent runtime errors. The compiler can't help you because, to it, any string is just as valid as another.

This is where the Bettys Bike Shop module on the kodikra learning path shines. It introduces you to Gleam's powerful type system, teaching you how to model your domain precisely. You'll leave behind the world of ambiguous data and enter a realm where the compiler becomes your most trusted ally, guaranteeing that illegal states are simply unrepresentable. This module isn't just about bikes; it's about a fundamental shift in how you write safe, maintainable, and robust software.


What Exactly is the "Bettys Bike Shop" Module?

The "Bettys Bike Shop" module is a hands-on project within the exclusive Gleam curriculum at kodikra.com. Its primary goal is to teach developers the art of domain modeling using one of Gleam's most powerful features: custom types (also known as algebraic data types or sum types).

Instead of just using generic types like String, Int, or List, you will learn to create your own types that perfectly describe the data in your specific problem domain. In this case, the domain is a bike shop. You'll define what a "Bike" is, what different kinds of bikes exist, and how to write functions that operate on this data with absolute certainty and safety.

The core challenge revolves around creating a system to manage the shop's inventory. This involves defining the types of bikes the shop sells and then implementing logic to handle them. It's a practical, focused exercise that solidifies foundational functional programming concepts in a way that's immediately applicable to real-world software development.


Why Domain Modeling with Custom Types is a Game-Changer

Using custom types isn't just a stylistic choice; it's a paradigm that fundamentally improves code quality. The compiler can check your logic at compile time, eliminating entire classes of bugs before your code ever runs.

Making Illegal States Unrepresentable

This is the central philosophy. With primitive types, you can easily represent nonsensical data, like a bike with 5 wheels. With custom types, you define a strict set of possibilities. If a bike can only be a Unicycle, Bicycle, or Tricycle, it's impossible to create a bike of any other kind. The type system enforces your business rules.

Self-Documenting Code

Well-defined types make your code incredibly readable. A function signature like pub fn number_of_wheels(bike: Bike) -> Int tells you everything you need to know. It takes a Bike and returns an Int. There's no ambiguity. Someone new to the codebase can understand the intent without reading a single line of implementation or documentation.

Compiler-Assisted Refactoring

Imagine the bike shop starts selling tandems. In a string-based system, you'd have to search your entire codebase for every instance of "bicycle" or related logic and manually update it, hoping you don't miss anything. In Gleam, you simply add a new variant, Tandem, to your Bike custom type. The compiler will then immediately show you every single place in your code that needs to be updated to handle this new case. It's like having a meticulous assistant guiding your refactoring process.

Fearless Concurrency

Gleam runs on the Erlang VM (BEAM), which is renowned for its concurrency and fault tolerance. Gleam's static type system complements this perfectly. When you pass immutable, well-defined data between concurrent processes, you drastically reduce the risk of race conditions and state corruption. The type system ensures that the messages being passed are exactly what the receiving process expects.


How to Build Betty's Bike Shop: A Deep Dive

Let's break down the technical implementation. The solution hinges on two key Gleam features: defining custom types with pub type and processing them with case expressions for pattern matching.

Step 1: Defining the Core Domain with a Custom Type

The first step is to model the central entity: a bike. We know the shop sells a few specific kinds. We can represent this finite set of possibilities using a custom type. In your Gleam file, you'll define it like this:


// In Gleam, `pub type` creates a new custom type that is visible
// to other modules. The variants are listed after the `=` sign.
// Each variant is a distinct "kind" of Bike.

pub type Bike {
  Unicycle
  Bicycle
  Tricycle
}

This simple definition is incredibly powerful. You've just created a new type, Bike, in your program. The only possible values for a variable of type Bike are Unicycle, Bicycle, or Tricycle. The compiler will now enforce this rule everywhere.

Step 2: Implementing Logic with Pattern Matching

Now that you have your data model, you need to write functions that work with it. Let's say you need a function to determine the number of wheels for any given bike. An if/else chain is clumsy and error-prone. Instead, Gleam encourages the use of case expressions for pattern matching.

A case expression checks a value against a series of patterns and executes the code for the first matching pattern. The Gleam compiler ensures that your patterns are exhaustive, meaning you must handle every possible variant of the custom type. This is a huge safety net!


// This function takes a value of our custom type `Bike`
// and is guaranteed to return an `Int`.

pub fn number_of_wheels(bike: Bike) -> Int {
  case bike {
    Unicycle -> 1
    Bicycle -> 2
    Tricycle -> 3
  }
}

If you were to add a Tandem variant to the Bike type but forget to update this function, Gleam would refuse to compile your code, pointing out the exact line and telling you that you missed a pattern. This feature alone prevents countless runtime errors.

Here is a visual representation of the pattern matching logic flow:

    ● Start: number_of_wheels(bike: Bike)
    │
    ▼
  ┌────────────┐
  │ Input: bike │
  └─────┬──────┘
        │
        ▼
    ◆ case bike { ... }
   ╱       │       ╲
  ├─ Unicycle?   ├─ Bicycle?   ├─ Tricycle?
  │        │       │
  ▼        ▼       ▼
┌──────┐ ┌──────┐ ┌──────┐
│ return 1 │ │ return 2 │ │ return 3 │
└──────┘ └──────┘ └──────┘
   ╲       │       ╱
    └──────┬──────┘
           │
           ▼
    ● End: return Int

Step 3: Managing the Inventory Collection

A bike shop has more than one bike. The inventory can be represented as a List of our custom Bike type. Gleam's standard library provides a rich API for working with lists.

Let's create a function to add a bike to the inventory. A key concept in Gleam and functional programming is immutability. You don't change the existing list; you create a new list with the bike added.


import gleam/list

// The inventory is just a list of our `Bike` type.
pub type Inventory = List(Bike)

// This function takes the current inventory and a bike to add,
// and it returns a *new* inventory list.
pub fn add_to_inventory(inventory: Inventory, bike: Bike) -> Inventory {
  // The `list.prepend` function is efficient for adding to the front.
  // It returns a new list without modifying the original.
  list.prepend(inventory, bike)
}

Here's how this immutable update process works:

    ● Start
    │
    ▼
  ┌──────────────────────────┐
  │ Initial State:           │
  │ inventory: [Bicycle, Unicycle] │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Action:                  │
  │ add_to_inventory(Tricycle) │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ Process:                 │
  │ list.prepend(inventory, Tricycle) │
  │ Creates a NEW list in memory. │
  └────────────┬─────────────┘
               │
               ▼
  ┌──────────────────────────┐
  │ New State:               │
  │ result: [Tricycle, Bicycle, Unicycle] │
  │ (Original list is unchanged) │
  └──────────────────────────┘
    │
    ▼
    ● End

Step 4: Putting It All Together

Now you can combine these pieces to build more complex logic, like calculating the total number of wheels in the entire inventory.


import gleam/list
import gleam/int

// We reuse our number_of_wheels function from before.

pub fn total_wheels(inventory: Inventory) -> Int {
  inventory
  // 1. Map over each bike in the list, converting it to its wheel count.
  |> list.map(number_of_wheels)
  // 2. Sum the resulting list of integers.
  |> int.sum()
}

pub fn main() {
  // Create an initial empty inventory.
  let initial_inventory: Inventory = []

  // Add some bikes using our immutable function.
  let inventory1 = add_to_inventory(initial_inventory, Bicycle)
  let inventory2 = add_to_inventory(inventory1, Tricycle)
  let final_inventory = add_to_inventory(inventory2, Bicycle)
  // final_inventory is now [Bicycle, Tricycle, Bicycle]

  // Calculate the total number of wheels.
  let total = total_wheels(final_inventory)
  // `total` will be 2 + 3 + 2 = 7
}

This code is clean, readable, and incredibly safe. Each function has a clear purpose, and the types guide the flow of data, preventing you from passing the wrong kind of data to a function.


Where This Pattern is Applied in the Real World

The concepts learned in the Bettys Bike Shop module are not just academic. They are used every day to build robust, large-scale software. The pattern of modeling a domain with custom types and processing it with pattern matching is a cornerstone of modern typed functional programming.

  • Web Backends: Modeling API request states (e.g., Loading, Success(data), Failure(error)). This is precisely how Gleam's built-in Result(value, error) type works, forcing you to handle both success and error cases.
  • E-commerce Systems: Representing the status of an order: Pending, Paid, Shipped(tracking_number), Delivered, Cancelled(reason). Notice how some variants can hold associated data.
  • UI Development: Managing component state in frameworks like React or Vue (often via state management libraries). A component's state can be defined as Initial, FetchingData, DisplayingContent(content), or ShowingError(message).
  • Compilers and Interpreters: A compiler's Abstract Syntax Tree (AST) is a large custom type. For example, an Expression type might have variants like Literal(Int), Variable(String), or BinaryOp(Operator, Expression, Expression).
  • Finite State Machines: Modeling any system with a finite number of states, such as a traffic light (Red, Yellow, Green) or a user's authentication status (Guest, Authenticated(user_id), TwoFactorPending).

When to Use Custom Types vs. Other Data Structures

Gleam provides several ways to structure data. Choosing the right one is key to writing effective code. The core distinction is between "is-a" relationships (sum types) and "has-a" relationships (product types).

Data Structure Represents Best For Example
Custom Type (Sum Type) An "is-a" relationship (OR) Modeling a value that can be one of several distinct kinds. type Bike { Unicycle | Bicycle } (A Bike is a Unicycle OR a Bicycle)
Record (Product Type) A "has-a" relationship (AND) Modeling a value that has a fixed set of named fields. type Customer { name: String, email: String } (A Customer has a name AND an email)
Tuple (Product Type) An "has-a" relationship (AND) Modeling a small, fixed-size collection of related but potentially different types where order matters and names are not needed. let coordinate: #(Int, Int) = #(10, 20)

Common Pitfalls and Best Practices

  • Pitfall: Using Tuples for Complex Data. While tuples are useful for returning multiple values from a function (like #(value, error)), they become unreadable for complex entities. If you have more than 2-3 elements, a record with named fields is almost always better for clarity.
  • Best Practice: Keep Custom Types Focused. A custom type should represent a single concept. Avoid creating monolithic "god types" that try to represent everything in your application. It's better to have several smaller, focused types that compose together.
  • Pitfall: Not Using the Exhaustiveness Check. Some languages have pattern matching that isn't exhaustive by default. A huge advantage of Gleam is that it forces you to handle all cases. Don't try to subvert this with a "catch-all" pattern (_ -> ...) unless you have a very good reason; it negates the safety benefits.
  • Best Practice: Combine Custom Types and Records. The most powerful models often combine both. For example, a variant of a custom type can contain a record: type PaymentMethod { CreditCard(CardDetails) | PayPal(email: String) } where CardDetails is a record.

Your Learning Path: From Novice to Pro

The Bettys Bike Shop module is a pivotal point in your journey. It takes the theoretical knowledge of Gleam's syntax and applies it to a practical problem, building a solid foundation for all future modules. Mastering this challenge will prepare you for more complex data structures and algorithms.

The progression within this module is straightforward, as it focuses on mastering one core concept through a single, comprehensive exercise.

  1. Foundational Challenge: The main exercise serves as both the introduction and the mastery test for custom types and pattern matching.

By completing this module, you will have gained a practical understanding of how to translate real-world requirements into safe, robust, and maintainable Gleam code. This skill is directly transferable to any feature you will build in the future.


Frequently Asked Questions (FAQ)

What is a custom type in Gleam?

A custom type (or sum type) is a type you define yourself that consists of a fixed set of possible variants. It allows you to model data that can be one of several distinct things. For example, type Status { Open | Closed } creates a new type named Status that can only ever be the value Open or Closed, preventing any other invalid values.

Why is pattern matching better than if/else chains?

Pattern matching with case expressions is superior for two main reasons. First, it's more expressive and readable when dealing with multiple conditions. Second, and more importantly, the Gleam compiler guarantees exhaustiveness. It forces you to handle every possible variant of a custom type, which means if you add a new variant later, the compiler will show you exactly which functions need to be updated. This eliminates a huge class of runtime bugs.

What does "exhaustive" mean in pattern matching?

Exhaustive means that your patterns in a case expression cover every single possible value of the type you are matching on. For a custom type, you must have a pattern for each variant. For a boolean, you must handle both True and False. This compiler guarantee is a core safety feature of Gleam.

How does Gleam's type system prevent runtime errors?

Gleam's static type system checks your code for type consistency before it ever runs. It prevents errors like trying to add a number to a string, calling a function with the wrong number or type of arguments, or accessing a field that doesn't exist on a record. By modeling your domain with custom types, you extend this safety net to your application's business logic, making entire categories of bugs impossible to write.

Can I add data to custom type variants?

Yes, absolutely! This is a very powerful feature. Variants can have "arguments" or "payloads" which let them carry associated data. For example: pub type UserEvent { Login(user_id: Int) | Logout | PostMessage(message: String) }. Here, the Login variant carries an integer, and the PostMessage variant carries a string, while Logout carries no extra data.

What's the difference between a `type` and a `type alias`?

A pub type, as used in this module, creates a brand new, distinct type with its own set of variants. A pub type alias, on the other hand, simply gives a new name to an existing type. For example, pub type alias UserID = Int doesn't create a new type; it just lets you use the name UserID as a more descriptive synonym for Int. The compiler still treats it as an Int.

Is Gleam suitable for large-scale applications?

Yes. Gleam is designed for building robust and maintainable systems. Its strong static type system, excellent compiler, and foundation on the battle-tested Erlang BEAM make it a great choice for applications where reliability, concurrency, and long-term maintainability are critical, such as web backends, distributed systems, and embedded software.


Conclusion: Beyond the Bike Shop

The Bettys Bike Shop module is far more than an academic exercise. It is your gateway to thinking like a functional programmer and leveraging a modern type system to its full potential. The skills you've honed here—modeling a domain, ensuring correctness with custom types, and writing clear logic with pattern matching—are the bedrock of building reliable software in Gleam and other typed functional languages.

You've learned to make illegal states unrepresentable, turning the compiler from a critic into a collaborative partner that helps you write better code. As you move forward in the Kodikra Gleam Learning Roadmap, you will see these patterns repeated, building upon this solid foundation to tackle even more complex and interesting problems.

Disclaimer: All code snippets and concepts are based on Gleam v1.x and the Gleam standard library. The language is actively developed, so always refer to the official documentation for the most current syntax and APIs.

Back to the main Gleam Guide


Published by Kodikra — Your trusted Gleam learning resource.