Utility Modules
These modules are written in Geblang and distributed under stdlib/. Import
them like any other module - the runtime resolves them by name.
freeze - Immutability
Import: import freeze;
The freeze module makes collections and class instances immutable at runtime.
Frozen values raise ImmutableError on any mutation attempt. ImmutableError
is a subclass of Error and is catchable with try/catch.
Functions
| Function | Returns | Description |
|---|---|---|
freeze.shallow(v) |
same type | Returns a frozen copy of v. Lists, dicts, sets, and class instances are frozen; nested collections are not. Primitives are returned unchanged. |
freeze.deep(v) |
same type | Returns a deeply frozen copy: all nested collections are also frozen recursively. |
freeze.isFrozen(v) |
bool |
Returns true if v is frozen or is a primitive (always immutable). |
All three functions work on both the evaluator and VM execution paths.
Mutation guards
Once frozen, mutation raises ImmutableError:
import freeze;
let x = [1, 2, 3];
x = freeze.shallow(x);
x[0] = 99; # throws ImmutableError: cannot modify frozen list
ImmutableError is catchable:
import freeze;
let x = freeze.shallow({"key": "value"});
try {
x["key"] = "changed";
} catch (ImmutableError e) {
io.println("caught: " + e.message);
}
.copy() — mutable shallow copy
All collection types support a .copy() method that returns an unfrozen
shallow copy, even when the original is frozen:
import freeze;
let frozen = freeze.shallow([1, 2, 3]);
let mutable = frozen.copy();
mutable[0] = 99; # ok — mutable is a new, unfrozen list
const auto-freeze
Declaring a variable with const automatically shallow-freezes collection
values at the point of declaration:
const config = {"host": "localhost", "port": 8080};
config["host"] = "example.com"; # throws ImmutableError
Primitives assigned with const are unchanged (they are already immutable).
@immutable class decorator
Apply @immutable to a class to freeze every instance after its constructor
returns. Field reads still work; any field assignment raises ImmutableError:
@immutable class Point {
int x;
int y;
func Point(int x, int y) { this.x = x; this.y = y; }
}
Point p = Point(3, 4);
io.println(p.x); # 3
p.x = 99; # throws ImmutableError: cannot modify frozen instance of Point
Use the wither pattern to produce modified copies from immutable classes:
@immutable class Point {
int x;
int y;
func Point(int x, int y) { this.x = x; this.y = y; }
func withX(int x): Point { return Point(x, this.y); }
func withY(int y): Point { return Point(this.x, y); }
}
Point a = Point(1, 2);
Point b = a.withX(10); # new immutable Point(10, 2)
stdlib/datetime.gb reference implementation
stdlib/datetime.gb provides @immutable class DateTime as a reference
implementation of the wither pattern. Import it like any source-stdlib module:
import datetime;
DateTime dt = DateTime(2026, 5, 17, 12, 0, 0);
DateTime next = dt.withMonth(6);
io.println(next.toString()); # 2026-06-17 12:00:00
result - Explicit Success/Failure Values
Import: import result;
Result<T,E> represents either a success value (ok) or a failure value
(err). Use it when you want to model operations that can fail without
relying on exceptions for control flow.
Constructors
| Function | Returns | Description |
|---|---|---|
result.ok(value) |
Result<T,E> |
Wrap a success value |
result.err(error) |
Result<T,E> |
Wrap a failure value |
Methods
| Method | Returns | Description |
|---|---|---|
isOk() |
bool |
true for success results |
isErr() |
bool |
true for error results |
unwrap() |
T |
Return the success value; throws ValueError if this is an error |
unwrapOr(fallback) |
T |
Return the success value, or fallback if this is an error |
unwrapErr() |
E |
Return the error value; throws ValueError if this is a success |
Example
import result;
import io;
func divide(int a, int b): Result<int, string> {
if (b == 0) {
return result.err("division by zero");
}
return result.ok(a / b);
}
let r = divide(10, 2);
if (r.isOk()) {
io.println(r.unwrap()); # 5
} else {
io.println(r.unwrapErr());
}
io.println(divide(5, 0).unwrapOr(-1)); # -1
Use Result at module or service boundaries where callers need to distinguish
success from specific error conditions. For unexpected runtime failures, prefer
throw/catch.
option - Optional Values
Import: import option;
Option<T> represents a value that may or may not be present. It makes
absence explicit in the type rather than relying on null checks.
Constructors
| Function | Returns | Description |
|---|---|---|
option.some(value) |
Option<T> |
Wrap a present value |
option.none() |
Option<T> |
An absent value |
option.ofNullable(value) |
Option<T> |
some(value) when non-null, otherwise none() |
Methods
| Method | Returns | Description |
|---|---|---|
isSome() |
bool |
true when a value is present |
isNone() |
bool |
true when no value is present |
unwrap() |
T |
Return the value; throws ValueError if absent |
unwrapOr(fallback) |
T |
Return the value, or fallback if absent |
orNull() |
T |
Return the value or null |
Example
import option;
import io;
func findUser(int id): Option<string> {
dict<int, string> db = {1: "Ada", 2: "Grace"};
return option.ofNullable(db.get(id));
}
let user = findUser(1);
io.println(user.isSome()); # true
io.println(user.unwrap()); # Ada
io.println(findUser(99).unwrapOr("unknown")); # unknown
io.println(findUser(99).orNull()); # null
option.ofNullable is the bridge between code that returns nullable values
and code that consumes Option. Use it at the boundary where a nullable result
enters option-aware code.
schema.Validator - Reusable Validators
Import: import schema.validator as sv;
Validator wraps a schema dict into a reusable object with structured error
reporting, built on top of the native schema.validate function. Create a
validator once and call it repeatedly for different values.
See the schema module reference for the
full list of supported schema keywords (type, required, properties,
items, minimum, maximum, minLength, maxLength, enum, etc.).
Constructors
| Function | Returns | Description |
|---|---|---|
sv.of(schemaDict) |
Validator |
Create a reusable validator from a schema dict |
sv.validate(value, schemaDict) |
dict |
One-shot validate without a Validator instance |
Methods
| Method | Returns | Description |
|---|---|---|
validate(value) |
dict |
Raw {valid: bool, errors: list<string>} result |
isValid(value) |
bool |
true when validation passes |
errors(value) |
list<string> |
All validation error messages |
fieldErrors(value, field) |
list<string> |
Errors matching a specific field path prefix |
Example
import schema.validator as sv;
import io;
let userSchema = sv.of({
"type": "object",
"required": ["name", "email"],
"properties": {
"name": {"type": "string", "minLength": 1},
"email": {"type": "string"},
"age": {"type": "number", "minimum": 0}
}
});
let valid = {"name": "Ada", "email": "ada@example.com", "age": 37};
io.println(userSchema.isValid(valid)); # true
let bad = {"name": "", "age": -1};
io.println(userSchema.isValid(bad)); # false
io.println(userSchema.errors(bad)); # ["name: minLength 1", "age: minimum 0", ...]
io.println(userSchema.fieldErrors(bad, "name")); # errors for the "name" field only
In a request handler, check isValid first and return error details from
errors() or fieldErrors() when validation fails.
random - Deterministic pseudo-random number generation
Import: import random;
The random module is a seedable PRNG suitable for simulation, sampling,
shuffling, procedural generation, fuzz inputs, and tests where
reproducibility matters. It is not cryptographically secure — for
security tokens, session IDs, salts, OTPs, and anything an attacker
shouldn't be able to predict, use the secrets module instead (see
12-security).
Module-level helpers
These call into a process-wide default generator. Seed it with
random.seed(n) for reproducibility.
| Function | Returns | Description |
|---|---|---|
random.seed(n) |
null |
Reseed the default generator |
random.next() |
int |
Random non-negative int63 |
random.intRange(min, max) |
int |
Uniform int in [min, max) |
random.float() |
float |
Uniform float in [0.0, 1.0) |
random.bool() |
bool |
Uniform coin flip |
random.choice(list) |
element | Random element from a non-empty list |
random.shuffle(list) |
list |
New list with elements in random order; original is unchanged |
import random;
import io;
random.seed(42);
io.println(random.intRange(1, 7)); # int in 1..6
io.println(random.float()); # float in 0..1
io.println(random.choice(["red", "green", "blue"]));
io.println(random.shuffle([1, 2, 3, 4, 5]));
Independent generators
For threads, tests, or parallel simulations that must not share state
with the default generator, create dedicated instances with
random.Generator(seed). The same module-level helpers accept the
generator as their first argument:
import random;
import io;
let rng = random.Generator(99);
io.println(random.intRange(rng, 0, 100));
io.println(random.choice(rng, ["heads", "tails"]));
io.println(random.shuffle(rng, [1, 2, 3]));
# Two generators with the same seed produce the same stream.
let a = random.Generator(7);
let b = random.Generator(7);
io.println(random.next(a) == random.next(b)); # true
functools - Functional Composition
Import: import functools;
Small higher-order helpers for composing and reshaping callables.
Functions
| Function | Returns | Description |
|---|---|---|
functools.pipe(fns) |
func |
Returns a function that applies fns left-to-right: pipe([f, g, h])(x) is h(g(f(x))). An empty list returns the identity function. |
functools.compose(fns) |
func |
Returns a function that applies fns right-to-left: compose([f, g, h])(x) is f(g(h(x))). |
functools.partial(fn, ...bound) |
func |
Returns a function that calls fn with bound prepended to its arguments. |
functools.memoize(fn, max = 128) |
func |
Returns an LRU-cached wrapper. Cache keys are derived from the JSON representation of the args, so memoized functions must take JSON-serialisable arguments. |
import functools;
import io;
let inc = func(int n): int { return n + 1; };
let double = func(int n): int { return n * 2; };
let square = func(int n): int { return n * n; };
let f = functools.pipe([inc, double, square]);
io.println(f(1)); # ((1+1)*2)^2 = 16
let add = func(int a, int b): int { return a + b; };
let add10 = functools.partial(add, 10);
io.println(add10(3)); # 13
int calls = 0;
let expensive = func(int n): int { calls = calls + 1; return n * n; };
let fast = functools.memoize(expensive);
fast(5); fast(5); fast(5);
io.println(calls); # 1 - only first call executed
memoize is best for pure functions of primitive or
collection-of-primitive arguments. For non-serialisable args (instances
of classes, generators, callables) write a thin wrapper that converts
to a key explicitly.