Gleam Bookshelf - HeyItWorked/babel-shelf GitHub Wiki
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.
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.
Once you set a variable, it's done. You can't change it.
let name = "Dune"
// name = "Foundation" ← this would be a compile errorIf you need a different value, you create a new variable:
let name = "Dune"
let name = "Foundation" // this "shadows" the old name — creates a new oneIn Go, book.Status = "reading" mutates the struct. In Gleam, you'd create a new Book with the updated field.
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, " + nameNotice: no return keyword. The last expression in a function is automatically returned.
pub fn greet(name) { ... } // other modules can use this
fn helper(name) { ... } // only this module can use thisSame idea as Go's uppercase/lowercase convention (CreateBook vs parseId).
This is the most important concept in Gleam. A custom type defines what shapes data can take.
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.
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
}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)book.title // "Dune"
book.status // ReadingSame as Python and Go. But you can never write to a field — everything is immutable.
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.
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."
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// 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.
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.
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.
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 dbtype Book imports the type for type annotations. WantToRead imports the constructor so you can use it directly instead of models.WantToRead.
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.
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]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.
| 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) { ... } |
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" |