Prelude / standard-library survey — the common core
A DESIGN SURVEY, not a spec. Surveys the load-bearing prelude constructs across comparable languages, then maps each against bang's CURRENT type-power to produce a prioritized menu of what to supply. Extends
docs/notes/stdlib-map.md(the third-stratum catalogue, gated by type-power) — this note asks the complementary question: given that ladder, which SPECIFIC everyday functions do peer preludes ship, and which are supplyable in bang TODAY? Established 2026-07-10.Sources of truth. Every "bang can/can't express X today" claim cites the generated reference
docs/reference/language.md(each of its examples is alake build-gated#guard, so it cannot drift) — thebangbinary was NOT built in this lane, so nothing here is empirically re-tested; claims are reference-cited. Peer-language claims cite the docs consulted (bottom).
1. The census — what peer preludes ship
Nine languages: mainstream (Haskell Prelude, OCaml Stdlib, Rust std prelude + Iterator, Elm core,
Gleam stdlib, F# Core) and the effects-native four (Koka std, Effekt stdlib, Unison base, Flix
stdlib). Frequency = count of the 10 languages shipping the construct in its prelude/core (✓ = ships,
· = absent or non-core). "≈" = ships under a different spelling (e.g. Rust Iterator::map).
construct Hs OCaml Rust Elm Gleam F# Koka Effekt Unison Flix freq note
────────────────────────────────────────────────────────────────────────────────────────────
LIST
map ✓ ✓ ≈ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 the universal core
filter ✓ ✓ ≈ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
foldl/foldr ✓ ✓ ≈ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 fold[l|r], reduce, fold_left
length/size ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
append/++ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
reverse ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 ← bang HAS (Str only)
head/tail ✓ ✓ ≈ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 9/10 Option-returning in most
take/drop ✓ · ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 9/10
zip ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
concatMap/flatMap✓ · ≈ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 9/10 flatMap / bind / concat_map
range ✓ · ≈ ✓ ✓ ✓ ✓ · ✓ ✓ 8/10 [a..b] / List.range
replicate ✓ · ≈ ✓ ✓ ✓ ✓ · ✓ ✓ 8/10
elem/contains ✓ ✓ ✓ ✓ ✓ ✓ ✓ · ✓ ✓ 9/10
any/all ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
OPTION / RESULT
map / andThen ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 ← bang HAS map (mapOption/mapResult)
withDefault ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 fromMaybe / unwrap_or / getOrElse
isSome/isOk ✓ ✓ ✓ ✓ ✓ ✓ · · ✓ ✓ 8/10
TUPLE
fst / snd ✓ ✓ ≈ ✓ ✓ ✓ ✓ · ✓ ✓ 9/10 ← dogfood #json top papercut
COMPARISON / ORDER
compare / Order ✓ ✓ ✓ ✓ ✓ ✓ ✓ · ✓ ✓ 9/10 ← bang HAS Eq→Order trait (Int)
min / max ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
NUMERIC
abs ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
mod / rem ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 ← issue #102 (no `%` / `mod` yet)
gcd ✓ · · · ✓ · ✓ · ✓ ✓ 5/10
STRING / CHAR
length ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10
concat / ++ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 ← bang HAS (concat)
toUpper/toLower ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ 10/10 char-level
split / join ≈ · ✓ ✓ ✓ ✓ ✓ · ✓ ✓ 8/10
isDigit/isAlpha ✓ · ✓ · ✓ · ✓ · ✓ ✓ 6/10 ← dogfood calc/json hand-rolled
IDENTITY / FN
id / const ✓ · · ✓ ✓ ✓ ✓ · ✓ ✓ 7/10 ← bang HAS id (⑦b example)
(∘) compose ✓ · · ✓ ✓ ✓ ✓ · ✓ ✓ 7/10 ← bang HAS compose (row-poly example)
The invariant core (10/10, every language): map · filter · fold · length · append · zip · any/all · Option-map · withDefault · min/max · abs · mod · string-length · string-concat · char-case. This is the floor any prelude is expected to clear.
2. The bang gate-map
Each high-frequency construct against bang's CURRENT type-power. Reference facts: generic List a +
map/fold/filter exist (docs/reference/language.md §Standard library note + the ADR-0079
generic-data examples); the injected free stdlib is only concat/reverse/eq over Str
(§Standard library table); curried let recs over-approximate to ! {Div} (#47); the force
convention is ($f) x and qualified $(Mod.op) x (§Modules); generic data arity ≤ 2 (§Types).
Classes: (a) SUPPLYABLE TODAY — System-F-typeable, arity ≤ 2, self-recursion only. (b) GATED on a named wall. (c) POST-V1 — needs typeclass-dispatch or IO.
Correction (2026-07-11, #105 list-prelude lane, machine-confirmed against the real binary):
every row below marked (a) for a List a CONSUMER (map/filter/foldr/foldl/length/
append/head/tail/take/drop/zip/any/all/concatMap/range/replicate) is
SUPPLYABLE only INSIDE one user program that monomorphizes List a to a concrete element type
(let rec length : List Int -> Int = …, the shape every corpus #guard/example actually uses,
e.g. examples/nqueens) — not as ONE shared Prelude.bang entry serving every element type.
A prelude entry needs a single signature that generalizes over a, and bang's surface has no
top-level ∀a. mechanism: pub let rec length : List a -> Int = … fails unknown type name 'a'
(traced to resolveTyG/resolveName, Bang/Frontend/TypeCheck.lean:1814-1873 — a bare type
variable resolves only via a data decl's own monomorphization substitution or a bounded
function's trait-bound variable, never a free/generalized one), and omitting the ascription
diverges the checker's self-recursion fixpoint. The one existing generic-plus-recursion mechanism,
ADR-0080's bounded fn f(xs) : List a -> a where Trait a = …, forces a trait bound these
functions don't want (none of length/append/take/drop/zip/range/replicate touch
elements). Every row below is re-classed (b), gated on this wall — the "class (a)" verdicts
that follow are the STALE per-program-only reasoning, kept for their ! {Div}/arity/shape
analysis (still correct once a real ∀-generalization or a bound-free ADR-0080 relaxation exists)
but WRONG about prelude-level supplyability. See stdlib-map.md's List-a row for the corrected
status and issue #120 (the ∀a-generalization-vs-bound-free-where fork) this wall was
escalated to — a kernel-adjacent typing-rule decision, out of this survey's/this lane's scope.
construct class wall / note (all citing docs/reference/language.md unless marked)
──────────────────────────────────────────────────────────────────────────────────────────────
map (List a) (b)* generic DATA + annotation-free/-driven CONSTRUCTION ship (ADR-0079/
0081); no shared generic CONSUMER — see the ∀a-wall correction above.
filter (List a) (b)* same wall as `map`.
foldr / foldl (b)* self-recursive, no bound needed ⇒ same ∀a-wall; `fold` WITH a trait
bound (`where Monoid a`) is the one shape that DOES ship (ADR-0080).
length (b)* same wall — per-program monomorphic supplyable (§ analysis below still
holds for that case), not prelude-shareable.
append (List a) (b)* same wall.
reverse (List a) (a) exists for Str (concrete, no generalization needed) — the `List a` form
hits the SAME ∀a-wall as `length`/`append` above; Str-only ships today.
head / tail (b)* returns `Option a`; NON-recursive (single match on `Nil`/`Cons`), so it
dodges the fixpoint-seeding half of the wall — but the ASCRIPTION half
still applies (`List a -> Option a` needs the same unbound `a`). Worth
a follow-up probe: does a non-recursive generic `List a` consumer (no
self-call) type-check without an ascription, mirroring `mapOption`'s
shape? Not tested this lane — flagged, not verified either way.
take / drop (b)* self-recursive ⇒ same ∀a-wall.
zip (b)* self-recursive (walks both lists) ⇒ same ∀a-wall; the arity-2 `Pair a b`
ceiling note still holds once the wall clears.
any / all (b)* self-recursive (structural fold) ⇒ same ∀a-wall; a PREDICATE arg makes
an `Eq`/`Ord`-style trait bound the WRONG fit even if bound-free-`where`
ships (a predicate function, not a carrier constraint).
concatMap/flatMap (b)* map then concat; inherits `map`'s wall.
elem / contains (a/c) needs `eq` on the element — the ONE row here where a trait bound
(`Eq a`) is semantically CORRECT, not a workaround — ADR-0080's bounded
`fn` shape may already fit once injected-generic-fn dispatch (class (c)
below) lands; unaffected by this correction.
range (Int→List) (a) self-recursive Int PRODUCER (`Int -> List Int`, concrete element type
Int, no `a` anywhere) — NOT affected by the ∀a-wall; genuinely (a).
replicate (b)* `a -> Int -> List a` — the ELEMENT is generic (unlike `range`), so it
DOES hit the ∀a-wall despite the shape-parallel to `range` the original
entry claimed; re-classed.
min / max (Int) (a) `if a < b …`; Int `<` exists (ref §operator table). Supplyable — already
SHIPPED in `Prelude.bang` since this survey (confirmed 2026-07-11).
abs (Int) (a) `if n < 0 then 0 - n else n`; supplyable (dogfood calc hand-rolled it).
mod / rem (b) ISSUE #102 — no `%` binop and no injected `mod`; today `t-(t/k)*k`.
Wall = a new δ-rule + parser-table row (shape (a) in #102), OR inject `mod`.
fst / snd (a) ISSUE (dogfood-json papercut) — trivially `let (a,_) = p in a`; supplyable
as injected `let`-bindings TODAY. Absence is a stdlib GAP, not a wall.
compare / Order (a/c) Int Eq→Order trait EXISTS (ref §Traits, Int instance). GENERIC `compare`
over any Ord a is (c) — needs the bounded-trait dispatch through the
free stdlib (traits work in decls; injected-generic-fn dispatch is the gap).
withDefault (a) `match o { None -> d, Some(v) -> v }`; supplyable, ⊥-row.
isSome / isOk (a) single match; supplyable.
string length/concat(a) concat SHIPPED (ref §Standard library). length = same shape.
char toUpper/isDigit(a) code-point arithmetic on `Char n` (ref §Strings idioms 97/48/…);
supplyable as `let rec` over Char. isDigit = `48 <= n && n <= 57`.
split / join (a) List Char recursion; supplyable, `! {Div}`.
id / const (a) id SHIPPED (ref ⑦b example). const = `fun x => fun _ => x`. Supplyable.
compose (∘) (a) SHIPPED (ref row-poly example, one generic `compose`). Supplyable.
──────────────────────────────────────────────────────────────────────────────────────────────
generic `Functor`/`fmap` (b) HKT — DECIDED (ADR-0082, Functor+Monad shipped); a PRELUDE-level generic
`fmap`/`>>=` over any `f` rides that rung. See stdlib-map.md §B.
typeclass `show`/`==` (c) post-v1 — needs typeclass-polymorphic dispatch through injected fns.
IO / print / readLine (c) post-v1 — IO is "a handler at the use site" (stdlib-map.md §C), no
console runtime in v1.
The effects-native surprise (the operator's cited question). Does map's effect row join the
element function's row? Three of the four effects-native languages answer YES, by row-propagation:
Koka map : (list<a>, (a) -> e b) -> e list<b> — map itself total; result row = f's row `e`
Unison List.map : (a ->{𝕖} b) -> [a] ->{𝕖} [b] — same: caller's ability threads through
Flix map : (a -> b \ ef, List[a]) -> List[b] \ ef — `ef` polymorphic; pure f ⇒ pure map
Effekt map[A,B](l){ f: A => B / {} }: List[B] / {} — DIFFERENT: signature rows are EMPTY;
block effects are handled at the CALL SITE
The Koka/Unison/Flix shape is exactly what bang's effect-row polymorphism already gives (ref: the
"ONE compose, generic over its effect row" example — a higher-order fn whose row is its argument's
row). So a bang map over an effectful element function threads the row the same way — bang is
already in the majority camp by construction, no new mechanism. Effekt's contextual/lightweight
polymorphism (empty rows, effects discharged where map is called) is the outlier and the biggest
surprise: it means Effekt's map signature literally cannot see the element effect, trading
row-visibility for a lighter surface. Bang's design (rows visible in the type, T ! {ρ}) sides with
Koka/Unison/Flix — the more common and the more type-transparent choice.
3. The recommendation
First slice — supplyable TODAY, ordered by dogfood-demand evidence
Status as of 2026-07-11: items 1, 4, 5, 8, 9, 10 SHIPPED (Prelude.bang, confirmed this lane);
items 2, 3, 6, 7 (the List a entries) are BLOCKED on the ∀a-generalization wall (§2 correction
above) — not class (a) as originally listed. Ordered by how loudly the dogfood corpus asked
(calc/json findings + issues #101/#102):
# construct why now (evidence) status
──────────────────────────────────────────────────────────────────────────────────────────
1 fst / snd dogfood-json TOP papercut ("first thing most reach for"); trivial. ✅ shipped
2 length (List a) 10/10 universal; every list program wants it. ⛔ ∀a-wall
3 append (List a) 10/10 universal; calc/json parsers hand-rolled concat-like joins. ⛔ ∀a-wall
4 abs (Int) both dogfooders hand-rolled `0 - n`; 10/10 universal. ✅ shipped
5 min / max (Int) 10/10; guard-heavy code (calc eval, bounds) wants them. ✅ shipped
6 head / tail Option-returning; 9/10; list destructuring boilerplate. ⛔ ∀a-wall*
7 take / drop 9/10; slicing shows up in every list-processing program. ⛔ ∀a-wall
8 isDigit/isAlpha/toUpper/toLower BOTH dogfooders hand-rolled code-point tests. ✅ shipped
9 id / const id already shipped; const is one line and completes the pair (7/10). ✅ shipped
10 withDefault (Option/Result) 10/10; pairs with the shipped Option prelude (ADR-0083). ✅ shipped
(*head/tail are non-recursive, so they dodge the fixpoint-seeding half of the wall — but the
ascription (List a -> Option a) still needs the same unbound a; unverified this lane whether
that half alone is passable, see the §2 correction's flagged follow-up.)
(mod is demand-rank-high — issue #102, stress session — but it is class (b), so it heads the second
slice, not this one.)
Second slice — keyed to which walls fall
wall that must fall unlocks
──────────────────────────────────────────────────────────────────────────────
#102 (`%` binop or `mod`) mod / rem / gcd — the numeric floor (10/10 for mod).
#101 (wildcard `_` arm) NOT a stdlib fn, but the enabler: multi-ctor container
fns (a generic `find`/`lookup` over a big sum) get far
cheaper to WRITE without spelling every ctor arm.
#97 item-2 (mutual let rec) mutually-recursive container walks (tree map/fold) read
top-down instead of leaf-first — parser/AST stdlib code.
#47 (Div elimination) take/drop/append/range STOP over-approximating to `! {Div}`
and become genuinely ⊥-row — a precision win, not a new fn.
bounded-trait injected dispatch GENERIC `elem`/`compare`/`min` over any `Ord a`/`Eq a`
(ref §Traits works in decls; (the class-(a/c) split above) — the traits EXIST; the gap
injected-generic-fn is the gap) is calling them through the free stdlib layer.
HKT prelude surface (ADR-0082) a prelude-level generic `fmap`/`>>=`/`traverse` (stdlib-map.md §B/§D).
The injection-mechanism fork (SURFACED — operator's call)
Today's stdlib is stdlibFnSrcs: source STRINGS injected into every program's scope at
type-check (ref §Standard library — concat/reverse/eq are let rec source snippets in
Bang/Frontend/TypeCheck.lean). The fork as the first slice grows past ~3 functions:
option pro con
──────────────────────────────────────────────────────────────────────────────────────────────
A. keep stdlibFnSrcs strings zero new machinery; already strings aren't type-checked in
(grow the list) works; no import needed isolation, no module boundary, no
(matches "free in every prog") `pub`/private, re-parsed every run;
a 30-fn prelude-as-a-string is a smell.
B. a real prelude MODULE rides ADR-0093 imports; the needs the module to be auto-`use`d
(`Prelude.bang`, auto-used) prelude becomes ORDINARY bang (no explicit `import Prelude`), and
LIBRARY CODE (invariant #5 — dogfood-calc found `use` won't hoist
"abstractions are values"); it a `pub let rec` (#97 item-3) — so B
type-checks, fmt's, tests like is BLOCKED on that fix for the
any module; dogfoods the module recursive fns that make up most of a
system on the stdlib itself. prelude.
Recommendation: B is the right answer — a Prelude.bang written in bang, auto-imported, is the
single-source-of-truth, invariant-#5-honest form (the stdlib should BE library code, not a string the
checker splices). It also dogfoods ADR-0093 on the most-used code in the language. Its real cost:
it is blocked today on use-hoisting-pub let rec (#97 item-3) — most prelude entries are
recursive, and use can't currently hoist them. So the honest sequencing is: ship the first slice
via mechanism A now (it works, unblocks the demand), and treat B as the target once #97 item-3
lands — then migrate the string-prelude into Prelude.bang and delete stdlibFnSrcs. Naming the
right answer (B) and its cost (blocked on #97-3), per the correctness-first discipline; A is the
named fallback, not a silent default. This is an operator call — I recommend, don't rule.
4. Proposed issues (titles + 2-line bodies — DO NOT file from this lane)
-
feat(stdlib): first-slice prelude functions —
fst/snd/length/append/abs/min/max/head/tail/take/drop/withDefault/id/const+ char-class kit. All class-(a) supplyable today (see stdlib-prelude-survey.md §3). Ship viastdlibFnSrcs(mechanism A) now; each is System-F-typeable, arity ≤ 2, self-recursive.fst/sndis the dogfood-json top papercut; the char kit (isDigit/toUpper) was hand-rolled in BOTH dogfooders. -
refactor(stdlib): migrate the injected prelude to a real
Prelude.bangmodule (auto-used). ReplacestdlibFnSrcsstring-injection with an ordinary bang module auto-imported into every program (invariant #5: the stdlib should BE library code). BLOCKED on #97 item-3 (usewon't hoist apub let rec) — most prelude fns are recursive. Land #97-3 first, then migrate + delete the strings. -
docs(stdlib): the effect-row-propagation contract for higher-order stdlib fns. Document that bang's
map/filter/foldthread the element function's effect row into the result (Koka/Unison/Flix shape, already true via bang's row-poly — stdlib-prelude-survey.md §2), NOT Effekt's empty-row/call-site-handled shape — so users know an effectful mapper's row surfaces.
Consulted (2026-07-10): Koka book + row-poly paper (koka-lang.github.io, arxiv 1406.2061 —
map : (list<a>,(a)->e b)->e list<b>); Effekt docs effect-polymorphism (effekt-lang.org —
map[A,B](l){f:A=>B/{}}:List[B]/{}); Unison docs abilities (unison-lang.org —
List.map : (a ->{𝕖} b) -> [a] ->{𝕖} [b]); Flix effect-polymorphism (doc.flix.dev —
map(f: a -> b \ ef, l): List[b] \ ef); Gleam gleam/list (hexdocs.pm); Elm core List +
Basics (package.elm-lang.org). Mainstream Haskell Prelude / OCaml Stdlib / Rust std prelude +
Iterator / F# Core from primary knowledge. bang facts: docs/reference/language.md (build-gated).