Gleam Bookshelf - HeyItWorked/babel-shelf GitHub Wiki

Gleam Bookshelf

Port: 8082 | Runtime: BEAM (Erlang VM) | Framework: Wisp + Mist | DB driver: pog

Gleam is a statically-typed functional language that runs on the Erlang virtual machine (BEAM). It looks like Rust but behaves like Erlang. No classes, no mutation, no exceptions — just functions, types, and pattern matching.


The Basics — Gleam in Simple English

Everything is an expression

In Go and TypeScript, you write statements — instructions that do things:

// Go — statements
x := 5
if x > 3 {
    fmt.Println("big")
}

In Gleam, everything returns a value. There are no statements, only expressions:

// Gleam — expressions
let x = 5
let message = case x > 3 {
  True -> "big"
  False -> "small"
}

The case expression returns a value — you can assign it directly. if/else doesn't exist in Gleam — you use case for everything.

Variables never change

Once you set a variable, it's done. You can't change it.

let name = "Dune"
// name = "Foundation"  ← this would be a compile error

If you need a different value, you create a new variable:

let name = "Dune"
let name = "Foundation"  // this "shadows" the old name — creates a new one

In Go, book.Status = "reading" mutates the struct. In Gleam, you'd create a new Book with the updated field.

Functions are the only way to do things

No classes. No methods. Just functions that take data in and return data out.

// Gleam — standalone function
pub fn greet(name: String) -> String {
  "Hello, " <> name
}
# Python equivalent
def greet(name: str) -> str:
    return "Hello, " + name

Notice: no return keyword. The last expression in a function is automatically returned.

pub means public

pub fn greet(name) { ... }   // other modules can use this
fn helper(name) { ... }      // only this module can use this

Same idea as Go's uppercase/lowercase convention (CreateBook vs parseId).


Types — The Heart of Gleam

Custom types (Gleam's version of enums + structs)

This is the most important concept in Gleam. A custom type defines what shapes data can take.

Simple custom type (like an enum)

pub type BookStatus {
  WantToRead
  Reading
  Finished
}

This says: "A BookStatus is one of three things. Nothing else." The compiler enforces this everywhere.

# Python equivalent
from enum import Enum
class BookStatus(Enum):
    WANT_TO_READ = "want to read"
    READING = "reading"
    FINISHED = "finished"
// Go — no real equivalent, just string constants
const StatusWantToRead = "want to read"
const StatusReading = "reading"
const StatusFinished = "finished"

The difference: In Go, status is a string — the compiler can't stop you from writing "banana". In Gleam, BookStatus is its own type — the compiler rejects anything that isn't one of the three variants.

Custom type with fields (like a struct)

pub type Book {
  Book(id: Int, title: String, author: String, status: BookStatus)
}

This defines a type called Book with one variant also called Book (common pattern). Each field has a name and a type.

# Python equivalent
@dataclass
class Book:
    id: int
    title: str
    author: str
    status: BookStatus
// Go equivalent
type Book struct {
    Id     int
    Title  string
    Author string
    Status string  // just a string, not a real enum
}

Creating a value

let book = Book(id: 1, title: "Dune", author: "Frank Herbert", status: Reading)
# Python
book = Book(id=1, title="Dune", author="Frank Herbert", status=BookStatus.READING)

Reading a field

book.title   // "Dune"
book.status  // Reading

Same as Python and Go. But you can never write to a field — everything is immutable.


Pattern Matching — Gleam's if/else Replacement

Pattern matching is how Gleam makes decisions. Instead of if/else if/else, you use case:

case book.status {
  WantToRead -> "On the list"
  Reading -> "Currently reading"
  Finished -> "Done!"
}
# Python equivalent
match book.status:
    case BookStatus.WANT_TO_READ: return "On the list"
    case BookStatus.READING: return "Currently reading"
    case BookStatus.FINISHED: return "Done!"

The killer feature: if you add a new variant to BookStatus (say Abandoned), every case expression that matches on BookStatus will fail to compile until you handle it. Go and TypeScript would silently fall through.

Matching with values

case int.parse("42") {
  Ok(number) -> "Got: " <> int.to_string(number)
  Error(_) -> "Not a number"
}

int.parse returns Result(Int, Nil) — either Ok(42) or Error(Nil). The case unpacks it. The _ means "I don't care what the error value is."


Result Type — How Gleam Handles Errors

There are no exceptions in Gleam. No try/catch. No throw. Instead, functions that can fail return a Result:

// Result has two variants:
// Ok(value)    — it worked, here's the value
// Error(error) — it failed, here's what went wrong

Compared to Go and TypeScript

// Go — returns (value, error)
book, err := getBookById(1)
if err != nil {
    // handle error
}
// TypeScript — throws exception
try {
    const book = await getBookById(1)
} catch (e) {
    // handle error
}
// Gleam — returns Result
case get_book_by_id(1) {
  Ok(book) -> // use the book
  Error(e) -> // handle error
}

The key difference: Go lets you ignore the error (book, _ := ...). TypeScript lets you skip the try/catch. Gleam forces you to match both cases — the code won't compile if you only handle Ok.

The use keyword — flattening Result chains

Without use, error handling gets nested:

case int.parse(id_string) {
  Error(_) -> bad_request()
  Ok(id) ->
    case get_book_by_id(id) {
      Error(_) -> not_found()
      Ok(book) ->
        json_response(book)
    }
}

With use, it flattens:

use id <- result.try(int.parse(id_string))
use book <- result.try(get_book_by_id(id))
json_response(book)

Read use x <- result.try(...) as: "Try this. If it fails, stop and return the error. If it works, put the value in x and keep going."

It's the same idea as Go's if err != nil { return err } — just without repeating it every time.


The Pipe Operator |>

Gleam's pipe takes the result of one function and passes it as the first argument to the next:

"hello"
|> string.uppercase
|> string.append(" WORLD")
// Result: "HELLO WORLD"

Without pipes:

string.append(string.uppercase("hello"), " WORLD")
# Python — no pipe, so you nest or use temp variables
result = "hello".upper() + " WORLD"

Pipes make data transformation readable — you read top to bottom instead of inside out.


Modules and Imports

Each .gleam file is a module. The module name matches the file path:

src/models.gleam       → import models
src/db.gleam           → import db
src/gleam_bookshelf.gleam → import gleam_bookshelf (the main module)
// In handlers.gleam
import models.{type Book, type BookStatus, WantToRead, Reading, Finished}
import db

type Book imports the type for type annotations. WantToRead imports the constructor so you can use it directly instead of models.WantToRead.


Strings and Concatenation

Gleam uses <> to join strings (not +):

"Hello, " <> name <> "!"

To convert other types to strings:

int.to_string(42)      // "42"
"Book #" <> int.to_string(book.id)

No implicit conversion — you must be explicit.


Lists

Gleam lists are linked lists (like Erlang's), not arrays:

let books = [book1, book2, book3]
let empty = []

// Get length
list.length(books)  // 3

// Map over them
list.map(books, fn(book) { book.title })  // ["Dune", "Foundation", "Neuromancer"]
# Python equivalent
books = [book1, book2, book3]
titles = [book.title for book in books]

How This All Connects to models.gleam

With these concepts, here's what models.gleam will look like and why:

// BookStatus — custom type with 3 variants
// The compiler will force every case match to handle all 3
pub type BookStatus {
  WantToRead
  Reading
  Finished
}

// Book — custom type with named fields
// Immutable — once created, never changed
pub type Book {
  Book(id: Int, title: String, author: String, status: BookStatus)
}

// status_to_string — needed because the DB stores strings, not custom types
// Pattern matching guarantees every variant is covered
pub fn status_to_string(status: BookStatus) -> String {
  case status {
    WantToRead -> "want to read"
    Reading -> "reading"
    Finished -> "finished"
  }
}

// status_from_string — returns Result because the string might be invalid
// Go would return (status, error), TS would throw — Gleam returns Result
pub fn status_from_string(s: String) -> Result(BookStatus, Nil) {
  case s {
    "want to read" -> Ok(WantToRead)
    "reading" -> Ok(Reading)
    "finished" -> Ok(Finished)
    _ -> Error(Nil)
  }
}

Why the converter functions? Go and TypeScript use raw strings for status — "reading" is the same in the code and the database. Gleam uses a proper type (Reading), so you need functions to convert between the type and the string the database stores. More code, but the compiler catches typos and missing cases.


Quick Reference — Gleam Syntax Cheat Sheet

Concept Gleam Go TypeScript
Variable let x = 5 x := 5 const x = 5
Function pub fn add(a, b) { a + b } func add(a, b int) int { return a + b } function add(a, b) { return a + b }
String concat "a" <> "b" "a" + "b" "a" + "b"
If/else case x { True -> ... False -> ... } if x { ... } else { ... } if (x) { ... } else { ... }
Enum type Color { Red Blue } const Red = "red" type Color = "red" | "blue"
Struct/Record type Foo { Foo(x: Int) } type Foo struct { X int } interface Foo { x: number }
Null/None Result or Option nil / zero value undefined / null
Error handling case f() { Ok(v) -> ... Error(e) -> ... } v, err := f(); if err != nil { ... } try { f() } catch (e) { ... }
Print io.println("hi") fmt.Println("hi") console.log("hi")
Pipe x |> f |> g g(f(x)) g(f(x))
Module import import gleam/io import "fmt" import { x } from "./y"
⚠️ **GitHub.com Fallback** ⚠️