Standard Library Guide

Looking for the full API reference? Every stdlib module, type, and function, with signatures and docstrings generated from source, lives at /docs/stdlib/. This page is a hand-written tour of the most commonly used modules.

March ships with 132 stdlib modules covering collections, strings, I/O, HTTP, cryptography, and more. This page provides an overview and quick reference for the most commonly used modules.

All stdlib modules are available without any import statement: use qualified access (List.map, String.length, etc.) or import/use to bring names into scope.


What do I use for…?

You know the task; this maps it to the module(s) that do it. (Don’t see your task? The full reference is searchable.)

I want to… Reach for Recipe
Parse CLI args System / Process (argv) CLI, Build a CLI
Read / write files File, Dir, Path Files
Parse a CSV Csv Files
Call an HTTP API HttpClient, Http HTTP
Parse a JSON response Json JSON API
Parse TOML / YAML config Toml, Yaml Config
Read environment variables Env, Config Config
Run a subprocess Process (run, run_stream) none
Hash / encode data Crypto, Base64 none
Work with dates / durations DateTime, Duration none
Structured logging Logger none
Run work concurrently Task, actor, Flow Concurrency
Transform a big collection in parallel List.pmap / pmap_n none
Process a large numeric array fast NativeArray, Simd Numeric Data

NativeArray and Simd

For numeric hot loops (summing a column, scaling an array, scanning bytes), reach for NativeArray instead of List/Array. It’s a flat, contiguous numeric array (Int/Float/f32/i32/u8 element widths) that march --compile can auto-vectorize: NativeArray.sum_float, NativeArray.map_int, NativeArray.map2_f32, and friends compile to real SIMD instructions on a compiled build, no ceremony required.

let arr = NativeArray.make_float(1_000_000, 0.0)
let doubled = NativeArray.map_float(arr, fn x -> x *. 2.0)
let total = NativeArray.sum_float(doubled)

NativeArray.sort_int sorts an Int array in place when nothing else holds it, and copies when something does. It is unstable, which no program can observe for plain numbers, and it recognises already-sorted, reversed and low-cardinality input instead of always paying n log n. NativeArray.sort_float does the same for Float arrays, ordering by IEEE 754 totalOrder: NaNs get a fixed place at the ends, and -0.0 sorts before 0.0. sort_i32 and sort_f32 are the same sorts on 4-byte elements (f32 is not widened to f64 to sort), and sort_u8 is a counting sort.

When you need guaranteed vector codegen rather than an optimizer decision (cross-lane structure with masks and select, a fused multiply-add, or byte-level scanning), reach for the Simd module instead: five 128-bit vector types (F32x4, F64x2, I32x4, I64x2, U8x16) with 127 operations between them, register-resident in compiled code.

See SIMD & Native Arrays for the full guide (what vectorizes, how to trigger it, the unvarnished performance story) and SIMD Benchmarks for the numbers, or jump straight to the API references: NativeArray, Simd.


Prelude (Auto-imported)

The Prelude module is automatically imported into every March program. These names are always in scope:

-- Diverging
panic("invariant violated")   -- terminates with an error
todo("not yet implemented")   -- typechecks as any type
unreachable()                 -- asserts a branch can't be reached

-- Option helpers
unwrap(Some(42))              -- 42, panics if None
unwrap_or(None, 0)            -- 0
unwrap_or_else(opt, fn () -> compute_default())

-- List basics
head([1, 2, 3])               -- 1, panics if empty
tail([1, 2, 3])               -- [2, 3], panics if empty
is_nil([])                    -- true

-- Combinators
identity(x)                   -- x
compose(f, g)                 -- fn x -> f(g(x))
flip(f)                       -- fn a b -> f(b, a)
const(x)                      -- fn _ -> x

List

list.march: the standard singly-linked list.

-- Construction
List.empty()                         -- []
List.singleton(42)                   -- [42]
List.repeat("hi", 3)                 -- ["hi", "hi", "hi"]
List.range(0, 5)                     -- [0, 1, 2, 3, 4]
List.range_step(0, 10, 2)            -- [0, 2, 4, 6, 8]

-- Access
List.head([1, 2, 3])                 -- 1  (panics if empty)
List.head_opt([1, 2, 3])             -- Some(1)
List.last([1, 2, 3])                 -- 3
List.nth([10, 20, 30], 1)            -- 20  (0-indexed; n : {Int | _ >= 0 && _ < len(xs)})
List.nth_opt([10, 20], 5)            -- None

-- Predicates
List.is_empty([])                    -- true
List.length([1, 2, 3])               -- 3
List.any([1, 2, 3], fn x -> x > 2)  -- true
List.all([2, 4, 6], fn x -> x % 2 == 0)  -- true
List.member([1, 2, 3], 2)           -- true

-- Transformation
List.map([1, 2, 3], fn x -> x * 2)          -- [2, 4, 6]
List.filter([1, 2, 3, 4], fn x -> x % 2 == 0) -- [2, 4]
List.filter_map(xs, fn x -> if x > 0 do Some(x * 2) else None end)
List.flat_map([1, 2, 3], fn x -> [x, x * 10])  -- [1, 10, 2, 20, 3, 30]
List.reverse([1, 2, 3])              -- [3, 2, 1]
List.append([1, 2], [3, 4])         -- [1, 2, 3, 4]
List.concat([[1, 2], [3], [4, 5]])   -- [1, 2, 3, 4, 5]

-- Folds
List.fold_left([1, 2, 3], 0, fn (acc, x) -> acc + x)   -- 6
List.fold_right([1, 2, 3], 0, fn (x, acc) -> x + acc)  -- 6
List.scan_left([1, 2, 3], 0, fn (acc, x) -> acc + x)   -- [0, 1, 3, 6]

-- Search
List.find([1, 2, 3], fn x -> x > 1)      -- Some(2)
List.find_index([10, 20, 30], fn x -> x == 20)  -- Some(1)

-- Iteration (side effects)
List.each([1, 2, 3], fn x -> println(int_to_string(x)))

-- Sorting
List.sort_by([3, 1, 2], fn (a, b) -> a < b)   -- [1, 2, 3]

-- Zipping
List.zip([1, 2, 3], ["a", "b", "c"])  -- [(1, "a"), (2, "b"), (3, "c")]
List.unzip([(1, "a"), (2, "b")])       -- ([1, 2], ["a", "b"])

-- Grouping
List.intersperse([1, 2, 3], 0)         -- [1, 0, 2, 0, 3]
List.take([1, 2, 3, 4, 5], 3)         -- [1, 2, 3]
List.drop([1, 2, 3, 4], 2)            -- [3, 4]
List.take_while([1, 2, 3, 4], fn x -> x < 3)   -- [1, 2]
List.drop_while([1, 2, 3, 4], fn x -> x < 3)   -- [3, 4]

-- Parallel (real multi-core parallelism in compiled code; the supplied
-- function MUST be safe to run concurrently)
List.pmap([1, 2, 3], fn x -> x * 2)              -- [2, 4, 6]  (parallel map)
List.pmap_n(urls, fn u -> fetch(u), 8)           -- parallel map, ≤8 tasks at once
List.pfilter([1, 2, 3, 4], fn x -> x % 2 == 0)  -- [2, 4]     (parallel filter)
List.preduce([1, 2, 3, 4], 0, fn (a, b) -> a + b)  -- 10  (combine MUST be associative)

The parallel operations are order-preserving and return exactly what their sequential counterparts (map/filter/fold_left) do. Below a configurable cutoff they fall back to the sequential version (no task-spawn overhead); above it the list is split into chunks, one task per chunk, run on the multithreaded scheduler. Real CPU parallelism happens in compiled code; the interpreter runs the tasks eagerly on one thread (correct, but sequential).

  • The cutoff is pmap_threshold() (default 1024), set at compile time with --pmap-threshold=N (forge passes it through via MARCH_PMAP_THRESHOLD).
  • pmap_n caps concurrency explicitly; use it when each element does heavy work (few elements, expensive per-element) so the size heuristic doesn’t apply.
  • preduce is a tree reduction: combine must be associative (combine(combine(x, y), z) == combine(x, combine(y, z))) with identity as its unit. Sum, product, max, min, string concat, set union qualify; subtraction and average do not. The compiler cannot check this; it’s the caller’s contract.

Tip: the LSP flags a pure List.map/List.filter as a pmap/pfilter candidate with a one-click “Convert to List.pmap” quick-fix.

See the Parallel Collections guide for how chunking works, the --pmap-threshold flag, when to parallelize, and the editor-driven detection.


String

string.march: String operations.

string_length("hello")              -- 5  (bare builtin, not under the String module)
String.join(["a", "b", "c"], "")    -- "abc"
String.join(["a", "b", "c"], ", ")  -- "a, b, c"
String.split("a,b,c", ",")          -- ["a", "b", "c"]
String.trim("  hello  ")            -- "hello"
String.trim_start("  hi")           -- "hi"
String.trim_end("hi  ")             -- "hi"
String.to_uppercase("hello")        -- "HELLO"
String.to_lowercase("HELLO")        -- "hello"
String.starts_with("hello", "he")   -- true
String.ends_with("hello", "lo")     -- true
String.contains("hello world", "world")  -- true
String.replace_all("hello", "l", "r")   -- "herro"  (replace() only replaces the first match)
String.slice("hello", 1, 3)         -- "ell"  (start, length — not start, end)
String.to_int("42")                 -- Ok(42)
String.to_float("3.14")             -- Ok(3.14)
String.repeat("ab", 3)              -- "ababab"
String.reverse("hello")             -- "olleh"

Map

map.march: HAMT-backed persistent map. O(log n) amortized operations.

Map operations that need key identity take an explicit comparator: cmp : k -> k -> Bool where cmp(a)(b) = true means a < b.

No comparator? If your keys are strings, ints, or any type where structural == is the right equality, use HashMap instead: identical API, no cmp argument anywhere.

let cmp = fn a -> fn b -> a < b   -- comparator for String keys

-- Construction
let m = Map.empty()
let m2 = Map.singleton("key", 42)
let m3 = Map.from_list([("a", 1), ("b", 2), ("c", 3)], cmp)

-- Access
Map.get(m3, "a", cmp)          -- Some(1)
Map.get_or(m3, "z", 0, cmp)   -- 0
Map.contains_key(m3, "b", cmp) -- true
Map.size(m3)                   -- 3
Map.is_empty(m)                -- true

-- Modification (returns new map)
let m4 = Map.insert(m3, "d", 4, cmp)
let m5 = Map.remove(m4, "a", cmp)

-- Traversal
Map.keys(m3)     -- ["a", "b", "c"] (hash order)
Map.values(m3)   -- [1, 2, 3]
Map.entries(m3)  -- [("a", 1), ("b", 2), ("c", 3)]
Map.fold(m3, 0, fn (acc, _k, v) -> acc + v)   -- 6

-- Transformation
Map.map_values(m3, fn v -> v * 10)            -- {"a": 10, "b": 20, "c": 30}
Map.filter(m3, fn _k -> fn v -> v > 1, cmp)  -- {"b": 2, "c": 3}
Map.merge(m3, m4, cmp)                        -- right takes precedence on conflict
Map.merge_with(m3, m4, fn a -> fn b -> a + b, cmp)  -- custom merge

-- Converting
Map.to_list(m3)   -- [("a", 1), ("b", 2), ("c", 3)]

LinearMap

A map for linear values, which Map cannot hold (see Linear Types). Every operation consumes the map and returns it; take is the only way a value comes out, and put returns the value it displaced. The map is itself linear, so it ends in drain, to_list or dispose.

let (old, m1) = LinearMap.put(LinearMap.empty_int(), 1, token)   -- old : Option(Token)
let (got, m2) = LinearMap.take(m1, 1)                              -- got : Option(Token)
let (n, m3)   = LinearMap.size(m2)
let (got2, slot) = LinearMap.take_slot(m3, 2)   -- value out, hole at key 2
let m4 = LinearMap.vacate(slot)                 -- or LinearMap.fill(slot, next)
LinearMap.drain(m4, 0, fn (acc, k, t) -> acc + close(t))

LinearMap.empty(cmp) takes a curried less-than comparator for the keys and stores it; empty_int() and empty_string() supply one. Keys must not be linear.

HashMap

hash_map.march: HAMT-backed persistent map using structural == for key equality and the built-in polymorphic hash function. No comparator needed at any call site. Key iteration order is hash-traversal order (not sorted, not insertion order).

Use Map when you need sorted output or a custom comparator. Use HashMap when you don’t want to thread a comparator everywhere, especially for string/int keys or for Enum.uniq / Enum.frequencies patterns.

-- Construction (no comparator arg anywhere)
let m = HashMap.new()
let m2 = HashMap.from_list([("a", 1), ("b", 2), ("c", 3)])

-- Access
HashMap.get(m2, "a")           -- Some(1)
HashMap.get_or(m2, "z", 0)    -- 0
HashMap.has(m2, "b")           -- true
HashMap.size(m2)               -- 3
HashMap.is_empty(m)            -- true

-- Modification (returns new map)
let m3 = HashMap.put(m2, "d", 4)
let m4 = HashMap.delete(m3, "a")

-- Counter pattern (no get-then-put dance)
let counts = List.fold_left(words, HashMap.new(), fn (acc, w) ->
  HashMap.update(acc, w, fn opt ->
    match opt do
    None    -> 1
    Some(n) -> n + 1
    end
  )
)

-- Traversal
HashMap.keys(m2)     -- ["a", "b", "c"] (hash order)
HashMap.values(m2)   -- [1, 2, 3]
HashMap.entries(m2)  -- [("a", 1), ("b", 2), ("c", 3)]
HashMap.fold(m2, 0, fn (acc, _k, v) -> acc + v)  -- 6

-- Transformation
HashMap.map_values(m2, fn v -> v * 10)
HashMap.filter(m2, fn _k -> fn v -> v > 1)
HashMap.merge(m2, m3)                               -- right takes precedence
HashMap.merge_with(m2, m3, fn va -> fn vb -> va + vb)

-- Converting
HashMap.to_list(m2)  -- [("a", 1), ("b", 2), ("c", 3)] (hash order)

Set

set.march: HAMT-backed persistent set. The set is always the first argument, and operations that need element identity take a trailing comparator cmp : a -> a -> Bool where cmp(a)(b) = true means a < b (same convention as Map).

let cmp = fn a -> fn b -> a < b

let s = Set.from_list([1, 2, 3, 4, 5], cmp)
Set.contains(s, 3, cmp)    -- true
Set.insert(s, 6, cmp)      -- {1,2,3,4,5,6}
Set.remove(s, 3, cmp)      -- {1,2,4,5}
Set.size(s)                -- 5
Set.is_empty(Set.empty())  -- true

let s1 = Set.from_list([1, 2, 3], cmp)
let s2 = Set.from_list([2, 3, 4], cmp)
Set.union(s1, s2, cmp)         -- {1,2,3,4}
Set.intersection(s1, s2, cmp)  -- {2,3}
Set.difference(s1, s2, cmp)    -- {1}
Set.is_subset(s1, s2, cmp)     -- false

Set.to_list(s)                       -- [1, 2, 3, 4, 5]
Set.fold(s, 0, fn (acc, x) -> acc + x)  -- 15

Option

option.march: Option(a) = None | Some(a).

Option.map(Some(5), fn x -> x + 1)     -- Some(6)
Option.map(None, fn x -> x + 1)        -- None
Option.flat_map(Some(5), fn x -> if x > 3 do Some(x) else None end)
Option.or_else(None, fn () -> Some(0)) -- Some(0)
Option.unwrap_or(None, 0)              -- 0
Option.unwrap_or_else(None, fn () -> compute())
Option.is_some(Some(1))               -- true
Option.is_none(None)                  -- true
Option.to_list(Some(42))              -- [42]
Option.to_list(None)                  -- []
Option.filter(Some(5), fn x -> x > 3)  -- Some(5)
Option.filter(Some(2), fn x -> x > 3)  -- None

Result

result.march: Result(a, e) = Ok(a) | Err(e).

Result.map(Ok(5), fn x -> x + 1)      -- Ok(6)
Result.map(Err("oops"), fn x -> x + 1) -- Err("oops")
Result.map_err(Err("x"), String.to_uppercase) -- Err("X")
Result.flat_map(Ok(5), fn x -> Ok(x * 2))   -- Ok(10)
Result.or_else(Err("x"), fn e -> Ok(0))  -- Ok(0)
Result.unwrap(Ok(42))                 -- 42  (panics on Err)
Result.unwrap_or(Err("e"), 0)         -- 0
Result.is_ok(Ok(1))                   -- true
Result.is_err(Err("e"))               -- true
Result.to_option(Ok(42))              -- Some(42)
Result.to_option(Err("e"))            -- None

For lightweight error propagation in chains, use the let? binding instead of Result.flat_map:

-- with Result.flat_map
Result.flat_map(String.to_int(s), fn n -> Result.flat_map(fetch(n), fn v -> Ok(v + 1)))

-- with let?
fn run(s : String) : Result(Int, String) do
  let? n = String.to_int(s)
  let? v = fetch(n)
  Ok(v + 1)
end

IO

io.march: Explicit I/O operations.

IO.puts("Hello, World!")         -- print with newline
IO.write("no newline")           -- print without newline
IO.warn("warning message")       -- print to stderr
IO.read_line()                   -- read a line from stdin -> String
IO.gets("> ")                    -- print prompt, read line
IO.inspect(any_value)            -- pretty-prints and returns any_value unchanged (for piping)

The println and print builtins are also always available.


Math

math.march: Mathematical functions.

int_abs(-5)                  -- 5  (Int abs is a bare builtin, not under Math)
Math.abs(-3.14)              -- 3.14  (Math.abs is Float-only)
Math.min_int(3, 5)           -- 3
Math.max_int(3, 5)           -- 5
Math.clamp_int(15, 0, 10)    -- 10
Math.sqrt(16.0)              -- 4.0
Math.pow(2.0, 10.0)          -- 1024.0
Math.exp(1.0)                -- 2.718...
Math.log(Math.e())           -- 1.0
Math.log2(8.0)                -- 3.0
Math.log10(1000.0)           -- 3.0
Math.floor(3.7)               -- 3.0
Math.ceil(3.2)                -- 4.0
Math.round(3.5)               -- 4.0
Math.sin(Math.pi() /. 2.0)   -- 1.0
Math.cos(0.0)                -- 1.0
Math.pi()                    -- 3.14159...  (pi/e/tau are 0-arg functions — call with `()`)
Math.e()                     -- 2.71828...
float_infinity()             -- Float infinity  (bare builtin, not under Math)
float_is_nan(float_nan())    -- true  (bare builtins; `0.0 /. 0.0` panics with "division by zero" rather than yielding NaN)

Crypto

crypto.march: Cryptographic primitives.

-- Hashing
Crypto.sha256("hello")              -- hex string
Crypto.sha512("hello")              -- hex string
Crypto.hmac(:sha256, key, message)  -- HMAC; algo is :sha256 or :sha512

-- Password hashing (PBKDF2 under the hood; stores salt + iterations)
let h = Crypto.hash_password("hunter2")     -- "pbkdf2:sha256:..." digest string
Crypto.verify_password("hunter2", h)         -- true (constant-time)

-- Randomness
Crypto.random_bytes(32)             -- Bytes of cryptographically random data
Crypto.random_hex(16)               -- random hex string (32 chars)

-- Encoding
Crypto.base64_encode("hello")       -- "aGVsbG8="
Crypto.base64_decode("aGVsbG8=")    -- Ok(Bytes)
Crypto.base64_url_encode("hello")   -- URL-safe Base64
Crypto.base64_url_decode(s)         -- Ok(Bytes)

-- Constant-time comparison (use for secrets / MACs)
Crypto.secure_compare(a, b)         -- Bool

UUID

uuid.march: UUID generation and parsing.

UUID.v4()                     -- generate a random UUID (a `UUID` value, not a bare String)
UUID.v5(namespace, name)      -- deterministic UUID from a UUID namespace + name
UUID.parse("550e8400-...")    -- Result(UUID, Atom): Ok(UUID(...)) or Err(:invalid)
UUID.to_string(uuid)          -- "550e8400-e29b-41d4-a716-446655440000"
UUID.version(uuid)            -- 4
UUID.is_valid("550e8400-...") -- true
UUID.to_string(UUID.nil())    -- "00000000-0000-0000-0000-000000000000"

JSON

json.march: JSON encoding and decoding.

type JsonValue =
    Null
  | Bool(Bool)
  | Number(Float)         -- both ints and floats decode to Number(Float)
  | Str(String)
  | Array(List(JsonValue))
  | Object(List((String, JsonValue)))

Json.parse("{\"key\": 42}")               -- Result(JsonValue, String)
Json.to_string(Object([("x", Number(1.0))]))  -- "{\"x\":1}"
Json.get(obj, "key")                      -- Option(JsonValue)

The module is Json (not JSON), and there is no encode/encode_pretty; to_string renders a JsonValue back to JSON text. encode_null/encode_bool/ encode_number/encode_int/encode_string/encode_array/encode_object are constructor helpers for building a JsonValue from a primitive, not string serializers.


HTTP Client

http_client.march: Make HTTP requests.

Http.get/Http.post/etc are pure: they just parse a URL into a Request (Result(Request(b), UrlError)), no network I/O. Actually performing a request goes through an HttpClient client value:

let client = HttpClient.new_client()

let resp = HttpClient.get(client, "https://api.example.com/data")
let resp = HttpClient.post(client, "https://api.example.com/data", body)

-- A custom method/headers request: build with Http, run with HttpClient.
let req  = Result.unwrap(Http.put("https://api.example.com/items/1", json_body))
let req2 = Http.set_header(req, "Content-Type", "application/json")
let resp = HttpClient.run(client, req2)

match resp do
  Ok(r) ->
    println("status: " ++ int_to_string(Http.response_status_code(r)))
    println("body: "   ++ Http.response_body(r))
  Err(e) ->
    println("error")
end

File System

file.march, dir.march, path.march:

-- File I/O (Result-based)
File.read("data.txt")              -- Result(String, String)
File.write("out.txt", "content")   -- Result((), String)
File.append("log.txt", "line\n")   -- Result((), String)
File.exists("config.json")         -- Bool
File.delete("temp.txt")            -- Result((), String)
File.copy("src.txt", "dst.txt")    -- Result((), String)

-- Directory
Dir.list("./src")                  -- Result(List(String), String)
Dir.mkdir("./output")              -- Result((), String)
Dir.exists("./data")               -- Bool

-- Path manipulation (pure, no I/O)
Path.join("src", "main.march")     -- "src/main.march"
Path.dirname("src/lib/foo.march")  -- "src/lib"
Path.basename("src/lib/foo.march") -- "foo.march"
Path.extension("foo.march")        -- "march"  (no leading dot)
Path.strip_extension("foo.march")  -- "foo"
Path.is_absolute("/usr/bin")       -- true

System

system.march: OS and runtime information.

System.os()                -- "macos" | "linux" | "windows" | "unknown"
System.arch()              -- "x86_64" | "aarch64" | "x86" | ...
System.cpu_count()         -- number of logical CPUs
System.monotonic_time()    -- Int (milliseconds since process start)
System.env("HOME")         -- Option(String)
System.put_env("KEY", "val")
System.argv()              -- List(String)
System.cwd()               -- String
System.pid()               -- Int (OS process ID)
System.exit(0)             -- terminate with exit code
System.cmd("ls", ["-la"])  -- Result(ProcessResult, String)
                            -- unpack with System.stdout/stderr/exit_code/ok

Logger

logger.march: Structured logging.

Logger.info("server started")
Logger.warn("connection retry")
Logger.error("database unreachable")
Logger.debug("processing request")
Logger.with_context(fn () ->
  Logger.put_context("request_id", "abc123")
  Logger.info("request received")
  process_request()
)

Vault

vault.march: Process-local key-value store backed by a mutable hash table. Used extensively in the stdlib for global mutable state.

A handle is Vault(v), phantom in the type of the values the table stores, so a table written at one type cannot be read back at another.

let t = Vault.new("counter_table")    -- Vault(v); or Vault.open(name), idempotent

Vault.put(t, "counter", 0)       -- pins v := Int
Vault.get(t, "counter")          -- Option(Int)
Vault.update(t, "counter", fn n -> n + 1)   -- fn is (v) -> v
Vault.delete(t, "counter")
Vault.keys(t)                    -- List(String) — all live keys (no prefix filter)
Vault.all(t)                     -- List((String, Int))

The element type is fixed where the handle is bound: a let-bound handle does not let-generalize, because a Vault is a process-global mutable cell. Two doors stay element-erased on purpose: new/open/whereis, which mint a handle from a name and so choose v rather than check it, and the ns_set/ns_get/ns_drop namespace-string API, which has no handle to carry v. Config (a heterogeneous store by design) opts out explicitly with a : Vault(v) annotation on its table getters.


Enum

enum.march: Elixir-inspired lazy enumeration over any Iterable.

Enum.map(items, fn x -> x * 2)
Enum.filter(items, fn x -> x > 0)
Enum.fold(items, 0, fn acc -> fn x -> acc + x)   -- combiner is curried
Enum.sort_by(items, fn a -> fn b -> a < b)        -- comparator: a < b
Enum.chunk_every(items, 3)     -- group into chunks of 3
Enum.chunk_by([1, 1, 2, 2, 3], fn x -> x)  -- [(1, [1, 1]), (2, [2, 2]), (3, [3])]
Enum.zip(items_a, items_b)
Enum.dedup(items)              -- remove consecutive duplicates
Enum.uniq(items)               -- remove all duplicates
Enum.take_while(items, pred)
Enum.drop_while(items, pred)
Enum.sum(nums)
Enum.product(nums)
Enum.scan(items, 0, fn acc -> fn x -> acc + x)
Enum.with_index(items)         -- [(x, 0), (y, 1), ...]
Enum.frequencies(items)        -- List((a, Int)): count of each item
Enum.min_by(items, fn x -> x.score)   -- Option(a)
Enum.max_by(items, fn x -> x.score)   -- Option(a)

Duration

duration.march: Time-span arithmetic.

let d = Duration.seconds(30)
let h = Duration.hours(2)
let w = Duration.weeks(1)

Duration.add(d, h)
Duration.subtract(h, d)
Duration.multiply(d, 3)
Duration.compare(d, h)     -- Int: negative, 0, positive
Duration.format(d)         -- "30 seconds"
Duration.to_milliseconds(d)  -- 30000  (Duration.milliseconds(n) is a *constructor*, not this accessor)

URI

uri.march: URI parsing and construction. The module is Uri (not URI).

let u = Uri.parse("https://example.com/path?k=v")
-- URI(scheme, host, port, path, query, fragment) — a plain ADT value, not a Result

Uri.scheme(u)                 -- "https"
Uri.host(u)                   -- "example.com"
Uri.path(u)                   -- "/path"
Uri.query(u)                  -- "k=v"
Uri.encode("hello world")     -- "hello%20world"
Uri.decode("hello%20world")   -- "hello world"
Uri.decode_query("k=v&a=b")   -- [("k", "v"), ("a", "b")]

Js.Dom (JS only)

dom.march: Browser DOM bindings for --target js builds. Auto-loaded; no import needed.

-- Query
Js.Dom.find("my-id")              -- Option(Js.Dom.Node)
Js.Dom.select("#nav")             -- Option(Js.Dom.Node)
Js.Dom.select_all(".item")        -- List(Js.Dom.Node)
Js.Dom.body()                     -- Js.Dom.Node

-- Construction
Js.Dom.create("div")              -- Js.Dom.Node
Js.Dom.text_node("hello")         -- Js.Dom.Node

-- Tree
Js.Dom.append(parent, child)
Js.Dom.prepend(parent, child)
Js.Dom.remove(el)
Js.Dom.remove_child(parent, child)

-- Content
Js.Dom.set_text(el, "hello")
Js.Dom.set_html(el, "<b>bold</b>")
Js.Dom.get_text(el)               -- String
Js.Dom.get_html(el)               -- String

-- Attributes and style
Js.Dom.set_attr(el, "data-x", "1")
Js.Dom.get_attr(el, "data-x")    -- Option(String)
Js.Dom.remove_attr(el, "data-x")
Js.Dom.add_class(el, "active")
Js.Dom.remove_class(el, "active")
Js.Dom.toggle_class(el, "open")
Js.Dom.set_style(el, "color", "red")

-- Events (Js.Dom.listen, not Js.Dom.on — there is no on_keydown/dispatch wrapper;
-- listen for "keydown" directly)
Js.Dom.listen(el, "click", fn ev -> ...)
Js.Dom.listen(el, "keydown", fn ev -> ...)
Js.Dom.prevent_default(ev)
Js.Dom.stop_propagation(ev)
Js.Dom.taps(el)                   -- List((Int, Int)) — drain buffered taps (poll per frame)
Js.Dom.key_presses()              -- List(String) — drain buffered keydown keys (poll per frame)
Js.Dom.store_get("save")          -- Option(String) — localStorage read
Js.Dom.store_set("save", data)    -- localStorage write
Js.Dom.pointer_pos(el)            -- (Int, Int) — live cursor position over el
Js.Dom.window_size()              -- (Int, Int) — window.innerWidth/innerHeight

Js.Dom requires needs Ffi because DOM calls are implemented as JS externs. It is only valid in --target js builds.


Js.Canvas (JS only)

canvas.march: 2D drawing bindings for --target js builds, wrapping the browser’s CanvasRenderingContext2D. Auto-loaded; no import needed.

-- Setup
Js.Dom.find("my-canvas")                          -- Option(Node), from Js.Dom
Js.Canvas.get_context(node)                       -- Option(Context)

-- State stack
Js.Canvas.save(ctx)
Js.Canvas.restore(ctx)
Js.Canvas.translate(ctx, 10.0, 10.0)
Js.Canvas.rotate(ctx, 0.5)
Js.Canvas.scale(ctx, 2.0, 2.0)

-- Style
Js.Canvas.set_fill_style(ctx, "#e74c3c")
Js.Canvas.set_stroke_style(ctx, "#3498db")
Js.Canvas.set_line_width(ctx, 2.0)
Js.Canvas.set_global_alpha(ctx, 0.8)
Js.Canvas.set_font(ctx, "16px sans-serif")

-- Rects
Js.Canvas.clear_rect(ctx, 0.0, 0.0, 480.0, 360.0)
Js.Canvas.fill_rect(ctx, 10.0, 10.0, 80.0, 40.0)
Js.Canvas.stroke_rect(ctx, 10.0, 10.0, 80.0, 40.0)

-- Paths
Js.Canvas.begin_path(ctx)
Js.Canvas.move_to(ctx, 0.0, 0.0)
Js.Canvas.line_to(ctx, 100.0, 100.0)
Js.Canvas.arc(ctx, 50.0, 50.0, 20.0, 0.0, 6.283185307179586)
Js.Canvas.quadratic_curve_to(ctx, 60.0, 0.0, 120.0, 40.0)
Js.Canvas.bezier_curve_to(ctx, 20.0, 0.0, 80.0, 0.0, 100.0, 40.0)
Js.Canvas.fill(ctx)
Js.Canvas.stroke(ctx)

-- Text
Js.Canvas.fill_text(ctx, "score: 0", 10.0, 20.0)
Js.Canvas.stroke_text(ctx, "score: 0", 10.0, 20.0)
Js.Canvas.set_text_align(ctx, "center")

-- Images
Js.Canvas.load_image("sprite.png")                -- Result(Image, String)
Js.Canvas.draw_image(ctx, img, 0.0, 0.0)
Js.Canvas.draw_image_scaled(ctx, img, 0.0, 0.0, 64.0, 64.0)

Js.Canvas requires needs Ffi because drawing calls are implemented as JS externs. It is only valid in --target js builds. Pair with Js.Dom.event_x/Js.Dom.event_y for canvas-relative pointer coordinates from a "pointerdown"/"click" listener.


Js.Audio (JS only)

audio.march: procedural sound-effect synthesis for --target js builds, wrapping the browser’s Web Audio API. Sounds are synthesized on the fly (tones, sweeps, filtered noise) rather than loaded from files: no assets, no licensing. Auto-loaded; no import needed.

Js.Audio.create()                                 -- Ctx
Js.Audio.resume(actx)                             -- unlock output; call from a user-gesture handler
Js.Audio.beep(actx, 440.0, 0.1, "sine")            -- flat tone: "sine"/"square"/"sawtooth"/"triangle"
Js.Audio.sweep(actx, 200.0, 800.0, 0.2, "square")  -- frequency ramp (chirps, risers, fall-offs)
Js.Audio.noise_burst(actx, 0.15, 600.0)            -- filtered white noise (impacts, explosions)
Js.Audio.set_volume(actx, 0.5)                     -- master gain 0.0 (mute) to 1.0

Js.Audio requires needs Ffi and is only valid in --target js builds. Browsers block audio output until a user gesture occurs on the page; call resume from inside your first tap/click handler (game loops that already gate their first frame on a tap, like a “tap to start” screen, get this for free).


Distributed OTP (cluster networking)

These modules implement the distributed-OTP transport layer (P1). They are stdlib modules loaded on demand via the module registry; no forge.toml dependency needed.

NodeIdentity

node_identity.march: A node’s stable identity for the cluster.

type Identity = { name : String, node_id : String, incarnation : Int }

NodeIdentity.make("worker@host", pubkey, 0)
-- { name: "worker@host", node_id: sha256(pubkey), incarnation: 0 }

NodeIdentity.encode(id)        -- List(Int)  (MessagePack bytes)
NodeIdentity.decode(bytes)     -- Result(Identity, String)

NetFrame

net_frame.march: Length-prefixed framing over TCP.

NetFrame.encode(payload)       -- List(Int)  (4-byte header + payload)
NetFrame.decode(buf)           -- Option((List(Int), List(Int)))

NodeCert

node_cert.march: node certificates for certificate-mode clusters, signed ed25519 by an operator key (forge cluster keygen, forge cluster cert).

NodeCert.keypair()                           -- Bytes (64-byte ed25519 secret key)
NodeCert.sign(cert, operator_sk)             -- Signed
NodeCert.verify(signed, operator_pub, now)   -- Result(Cert, String); now in unix seconds
NodeCert.to_text(signed) / from_text(text)   -- base64 wire form (MARCH_NODE_CERT)
NodeCert.has_role(cert, "Proto.Role:offer")  -- Bool
NodeCert.sign_revocation(rev, operator_sk)   -- SignedRevocation
NodeCert.is_revoked(cert, revocations)       -- Bool

SessionAP

session_ap.march: the access-point authorization check of a certificate-mode cluster (who may offer and who may initiate a role). SessionNode’s access points call it; it is pure.

SessionAP.authorize(Some(cert), "Checkout", "Ledger", SessionAP.offer_mode())
                                              -- Result((), String)
SessionAP.permission("Checkout", "Ledger", SessionAP.initiate_mode())
                                              -- "Checkout.Ledger:initiate"

ClusterAuth

cluster_auth.march: how a node authenticates in the cluster handshake, in either mode, and the per-connection MAC keys the handshake derives.

ClusterAuth.Secret(secret) / ClusterAuth.Certified(credentials)  -- Auth
ClusterAuth.prove(secret, nonce)         -- String (HMAC-SHA256 hex)
ClusterAuth.verify(secret, nonce, proof) -- Bool
ClusterAuth.frame_keys(ikm, transcript, my_nonce, peer_nonce)  -- FrameKeys (HKDF-SHA256)

Handshake

handshake.march: Pure authenticated handshake protocol (no I/O).

Handshake.make_hello(identity, nonce)    -- Handshake.Hello
Handshake.encode_hello(hello)            -- List(Int)
Handshake.decode_hello(bytes)            -- Result(Hello, String)
Handshake.authenticate(secret, our_nonce, peer, peer_proof)
-- Result(NodeIdentity.Identity, String)

PeerRegistry

peer_registry.march: One-connection-per-peer table.

type Peer = { node_id : String, identity : NodeIdentity.Identity, fd : Int }
type Registry = Registry(Map(String, Peer))

PeerRegistry.empty()
PeerRegistry.add(reg, peer)           -- Registry
PeerRegistry.lookup(reg, node_id)     -- Option(Peer)
PeerRegistry.remove(reg, node_id)     -- Registry
PeerRegistry.peers(reg)               -- List(Peer)

NetKernel

net_kernel.march: TCP socket transport for the net-kernel (framing + handshake).

NetKernel.fresh_nonce()               -- String (random hex)
NetKernel.send_frame(fd, payload)     -- Result((), String)
NetKernel.recv_frame(fd, buf)         -- Result((List(Int), List(Int)), String)
NetKernel.handshake(fd, my_id, secret, my_nonce)
-- Result(NodeIdentity.Identity, String)

ClusterConn

cluster_conn.march: Connection lifecycle: dial or accept, handshake, register.

Requires needs IO.NetListen (for tcp_listen/tcp_accept builtins).

ClusterConn.listen(port)
-- Result(Int, String)  -- listen_fd

ClusterConn.connect_to_peer(reg, host, port, my_id, secret)
-- Result((PeerRegistry.Registry, PeerRegistry.Peer), String)

ClusterConn.accept_one(reg, listen_fd, my_id, secret)
-- Result((PeerRegistry.Registry, PeerRegistry.Peer), String)

-- The control/data split: two connections per peer.
ClusterConn.connect_split(reg, host, port, my_id, secret)
ClusterConn.accept_split(reg, listen_fd, my_id, secret)
-- Result((PeerRegistry.Registry, PeerRegistry.Peer), String)

-- The same, giving up after timeout_ms (<= 0: no limit) with no connection
-- pending or a handshake unfinished; a peer that connects and says nothing
-- is refused instead of holding the caller.
ClusterConn.connect_split_within(reg, host, port, my_id, secret, timeout_ms)
ClusterConn.accept_split_within(reg, listen_fd, my_id, secret, timeout_ms)

tcp_listen and tcp_accept are builtins in the IO.NetListen capability group:

tcp_listen(port)       : Result(Int, String)  -- bind + listen, return fd
tcp_accept(listen_fd)  : Result(Int, String)  -- block until client, return fd
tcp_accept_timeout(listen_fd, timeout_ms) : Result(Int, String)
                       -- as tcp_accept; Err("tcp_accept: timed out") if no client
                       -- connects within timeout_ms (<= 0 waits for ever)

tcp_recv_exact honours a receive timeout set with tcp_set_recv_timeout: the whole read fails with Err("tcp_recv_exact: timed out") once it passes.


Module Quick Reference

Module Purpose
Prelude Auto-imported helpers
List Singly-linked list operations
String String manipulation
Map HAMT-backed key-value map
Set HAMT-backed set
Option Maybe type helpers
Result Either type helpers
Enum Lazy enumeration
Sort Timsort, Introsort, specialized small sorts
Math Arithmetic and transcendental functions
IO Console I/O
System OS/runtime info
File File I/O
Dir Directory operations
Path Path manipulation
Crypto SHA, HMAC, PBKDF2, random
UUID UUID v4/v5
Json JSON encode/decode
Base64 Base64 encode/decode
Uri URI parsing/construction
Duration Time-span arithmetic
Logger Structured logging
Vault Process-local KV store
Http HTTP client
HttpServer HTTP/WebSocket server
Task Structured concurrency
Seq Lazy sequences
IOList Lazy string builder
Random Random number generation
Regex Regular expressions
AhoCorasick Multi-pattern string search (build once, scan many)
BigInt Arbitrary-precision integers
Decimal Exact decimal arithmetic
Csv CSV parsing
DateTime Date and time
Js.Dom Browser DOM bindings (JS only, auto-loaded for --target js)
Js.Canvas 2D canvas drawing bindings (JS only, auto-loaded for --target js)
NodeIdentity Cluster node identity (name, node_id, incarnation)
NetFrame Length-prefixed TCP framing
ClusterAuth HMAC shared-secret challenge/response
Handshake Pure authenticated handshake protocol
PeerRegistry One-connection-per-peer table
NetKernel TCP framing + handshake transport
ClusterConn Dial/accept + handshake + register lifecycle
Gen Property test generators (Hedgehog-style)
Check Property test runner with shrinking
Test Test assertion helpers

Next Steps

  • Property Testing: write property-based tests with Gen and Check
  • REPL: explore the stdlib interactively
  • Interfaces: how stdlib types implement interfaces
  • Tooling: forge search to find functions by type or name