The Monad language. Dependent types, functional programming compiled with LLVM. Hobby project. monad-lang.org
dependent-types language compiler programming-language functional-programming
monad docs src stdlib.md
13 kB
Markdown
at main

The Standard Library #

The standard library is split into two directories, and the split is a rule, not a convention:

  • init/ — pure, portable core. Code that must work in any environment, including wasm and embedded targets. No OS-specific natives.
  • std/ — OS-specific implementations and genuine side effects.

IO the type lives in init/; IO.println lives in std/, because printing touches the operating system.

Roughly 440 public definitions, 35 classes, and 132 instances across the two, not counting their test modules.

What Is Available Without Importing #

Twelve modules load ambiently: the prelude, id, io, number, math, string, list, the init hub, std.path, std.io, std.process, and the std hub.

Everything else needs an explicit import, and the re-export hubs do not help: init/lib.mo re-exports only id, io, number, math, string, list, and std/lib.mo only std.path, std.io, std.process. So init.foldable, init.optics, init.meta, and all of std beyond those three — std.array included — are opt-in.

init/ — pure and portable #

prelude — ambient, not importable #

The core types (Bool, List, Option, Result, Pair, Nat, String, Char, Unit, Void, Any, Vec, Eq, True, the numeric primitives) and the core classes (Functor, Applicative, Monad, MonadState, MonadLift, MonadLiftT, IndexedMonad, IndexedMonadState, IndexedMonadLift, FromListLiteral, HAdd, Add, Sub, HMul, Div, Append, BEq, BOrd, ToString, Hashable).

Ptr lives here too: an opaque machine word for #[extern "c"] functions that take or return a pointer (an SSL* from OpenSSL, a FILE*). The only value constructible in pure code is Ptr.null — the NULL a C API's unused callback slot wants. A Ptr is untyped: passing an SSL_CTX* where an SSL* belongs type-checks and segfaults, so wrap handles in a newtype (motes/tls's Tls.Stream is the worked example).

MonadState and IndexedMonadState take the monad as their only parameter — the state type is an implicit forall — so that instance resolution can key on a concrete monad head. examples/state_monad.mo and examples/indexed_monads.mo show both in use.

Functions: Bool.not/and/or, List.is_empty/append/first/last/flatten/tail/map, Option.get_or_default, Nat.add/sub/mul/eq, fun_apply/apply_fun.

init (init/lib.mo) — the hub #

Re-exports the modules below, defines the From class, BEq (Option A), and rebinds infix (+) := I64.add.

init.number / init.math #

The numeric workhorse — about 600 lines of instances. For each of I8, I16, I32, I64, U8, U16, U32, U64, F32, F64: add, sub, mul, div, beq, lt, gt, to_string natives plus Add, Sub, HMul, Div, BEq, BOrd, and ToString instances. Also Hashable I64/U64, U32 bitwise operations (and, or, xor, shl, shr), and width conversions.

init.string #

String.beq, concat, concat_all, concat_list, length, is_empty, slice, drop, get, get_char, to_lowercase, starts_with, ends_with, contains, repeat, reverse, trim, find_last, to_list/from_list, to_chars/from_chars (declared but not implemented — these fail at run time with unknown native), hash, plus List.reverse and List.singleton. Instances: BEq, BOrd, ToString, Add, Append, Hashable.

init.list #

List.get. Most list operations are in the prelude or std.list.

init.id #

The identity monad: Id A, Id.run, and Functor/Applicative/Monad instances. Useful as the trivial case when writing code generic over a monad.

init.foldable — not ambient #

Semigroup (combine), Monoid (mempty), Foldable (foldr, foldl), Traversable (traverse). Instances for List, Option, and String.

init.optics — not ambient #

Van Laarhoven-style optics: the Lens and Prism types, with lens, view, set, over, preview, review, over_prism, set_prism. #[derive Lens] generates one lens per struct field.

init.meta — not ambient #

The reflection-as-data types (TypeInfo, CtorInfo, FieldInfo, Expr, MatchArm, Param, Decl) that the macro system passes to derive backends. See Macros and Derive.

io #

type IO A, def IO.pure, and instance Monad IO. Nothing else — deliberately. Construct with IO.pure, unwrap with Monad.bind; the io constructor itself is not for call sites and is meant to become an implementation detail.

std/ — OS-specific #

std.io — ambient #

IO.println and the filesystem: read_file, write_file, file_exists, is_dir, list_dir (all Path-typed), plus IO.get_env and IO.current_time (monotonic milliseconds — only differences are meaningful).

It also holds the entire TCP surface: the opaque Socket and Listener types and nine blocking natives — tcp_connect, tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close, tcp_close_listener, tcp_local_port, and tcp_fd (the raw descriptor behind a Socket, typed extraction rather than a lookup — the thing an FFI consumer such as motes/tls hands to SSL_set_fd). These are implemented only by the self-hosted backend; the Rust bootstrap host has no TCP at all, so a socket test cannot run under cargo run -- test. The IO Monad has the signatures, the two worked snippets, and the caveats.

std.path — ambient #

A validated Path newtype: Path.of (the validating constructor), to_string, is_absolute, join, with_suffix, beq, and BEq Path.

std.process — ambient #

exec_cmd and process_id.

std.list — not ambient #

List.length, filter, any, all, sum, find_by, filter_map, contains_by, intercalate, dedup_by; instances BEq (List A) and Append (List A).

std.map — not ambient #

A real persistent-map implementation, not a stub:

  • BTreeMap K V — AVL-balanced, with insert, lookup, delete, fold, to_list, and instance [BOrd K] Map BTreeMap
  • HashMap K V — 256 buckets, with instance [Hashable K, BOrd K] Map HashMap
  • class Map (M := HashMap) — empty, insert, lookup, delete

std.base — not ambient #

The Ordering type and the value classes: Ord (three-way compare), Semigroup, Monoid, Default, Enum, Bounded, with instances for Ordering, String, Bool, and every numeric width.

Note that Semigroup and Monoid are declared twice — here and in init.foldable — with different method names. They are unrelated classes that share a name. Import one or the other, not both.

std.show and std.debug — not ambient #

class Show A { def show : A -> String } and class Debug A { def debug : A -> String }. Debug is a Rust-style diagnostic representation and quotes strings; Show is a display string. The prelude's ToString is what the numeric types implement.

std.derive — not ambient #

derive_beq_meta, derive_bord_meta, derive_debug_meta, derive_lens_meta and the defmacros that wrap them. Must be imported for #[derive ...] to resolve — see Macros and Derive.

std.test — not ambient #

Test.assert (condition : Bool) : Bool := condition. That is the entire assertion library — an identity function. There is no assert_eq, no failure message, no matcher DSL. In practice most tests just return a Bool expression directly, which is why Test.assert has only a handful of callers across 1600 tests.

std.bench — not ambient #

Bench.now, Bench.since, Bench.report, Bench.report_since — all IO, over the monotonic clock. Micro-benchmarks live in bench/ and are deliberately excluded from the ordinary test sweep.

std.array — not ambient #

Array A, a native-backed sequence with O(1) length and get, alongside a mutable ArrayBuilder A for filling one under IO.

Function Cost Notes
Array.new n fill, Array.from_list, Array.empty O(n) construction
Array.length, Array.get O(1) get returns Option A; out of range is none, never a crash
Array.get_or a i fallback O(1) total accessor
Array.set a i v O(n) persistent — copies, leaves a untouched, ignores an out-of-range index
Array.push, Array.map, Array.foldl, Array.to_list O(n) an Array is not a growable buffer
Array.builder n fill, Array.set_in_place, Array.freeze see below the mutable half, all under IO

Array.set_in_place is O(1) in a compiled binary and O(n) under the bootstrap host's interpreter, which copies on write. Both are correct; only the cost differs, so a hot fill loop belongs in compiled code. Array.freeze deliberately copies, so writing to the builder afterwards cannot disturb the frozen array.

Array implements no classes at all — Array.map and Array.foldl are ordinary functions, not Functor/Foldable methods.

std.bytebuf — not ambient #

ByteBuf, a flat byte buffer the C FFI can address — the thing a #[extern "c"] function that takes a void* plus a length wants (SSL_read/SSL_write in motes/tls are the first consumers; an Array U8 exposes no backing store). Values are opaque like Socket: never pattern-match, compare or print one. A buffer records its length — length is what it was created with, and to_list's len is clamped to it, so an over-large len cannot read past the allocation (pass SSL_read's return to read only what came back).

Function Notes
ByteBuf.alloc n n zeroed bytes
ByteBuf.of_list xs copy into a fresh buffer
ByteBuf.to_list b len the first len bytes, clamped to length
ByteBuf.length b how many bytes the buffer was created with
ByteBuf.ptr b the raw payload address, a Ptr — compiled backend only
ByteBuf.free b a no-op compiled (Boehm reclaims); a real drop interpreted

The two backends hold different words and a value never crosses between them: compiled it is a pointer to the heap kind the MLIR plan's Buf E generalises (element kind, recorded length, 64-byte-aligned atomic payload), interpreted a registry id (the Fiber/Scope convention) whose length is its Vec's.

std.ansi — not ambient #

Terminal colours: the Color, Style, and Modifier types, with red, green, yellow, bold, dim, Ansi.fail, Ansi.pass, warn, colored, the escape/color_fg_code/color_bg_code/style_code builders, and an environment-aware colors_enabled (NO_COLOR wins, then FORCE_COLOR, then TERM=dumb).

Ansi.fail and Ansi.pass were once bare fail and pass. The dotted names are load-bearing: a bare fail here captured ParseResult.fail elsewhere in the tree during module qualification and produced a segfault.

std.sha256 — not ambient #

A complete SHA-256 implementation in pure Monad, with no natives, built on the U32 bitwise operations. A good demonstration that the numeric tower is real.

std.concurrent.fiber / std.concurrent.combine — not ambient #

Fibers and structured-concurrency combinators. Read Concurrency before using them — compiled binaries run fibers on real OS threads, while the bootstrap host's interpreter is lazy and cooperative, so the two backends disagree about what a forkIO means.

Gaps Worth Knowing About #

  • Assertions. Test.assert is an identity function; there is no equality assertion or failure message.
  • No Iterator class. Iteration is Foldable or direct recursion.
  • No stdin. IO can print and touch files, but cannot read a line.
  • Traversable has no instances at all — the class is declared and nothing implements it.
  • Duplicate Show. std.list declares a local class Show A alongside its import of std.show's. Prefer std.show.
  • Three declared-but-unimplemented natives: eq_rec (so Eq.rec cannot be called), string_to_chars, and string_from_chars.
  • TCP has no timeout and no non-blocking mode: tcp_connect, tcp_accept and tcp_read block until they complete, so a peer that connects and then stays silent holds a fiber and its OS thread indefinitely. And TCP is self-hosted only — the Rust host cannot run a socket at all.

See the Maturity Matrix for the summary view.