jolt/docs/spec/09-core-library.md
Yogthos 11318ee386 Collection fns: JVM-faithful return types + laziness
A type-aware audit (~190 collection expressions vs reference Clojure) found four
divergences the corpus missed — value-equality (= [0 1] '(0 1)) hides type and
laziness differences. Fixed, with type-predicate + over-infinite corpus rows that
pin them.

- partition-all [n coll] built vector chunks; JVM chunks are seqs. (The [n step
  coll] arity was already correct, as is the partition-all transducer, whose
  chunks are vectors in JVM too.) Now builds seq chunks.
- replace always returned a vector (mapv) and was eager; JVM is type-preserving —
  a vector maps to a vector, any other seqable to a lazy seq.
- sequence eagerly realized its source (into-xform), so (first (sequence (map inc)
  (range))) hung. Rewrote as a transformer iterator: pull one input at a time,
  buffer the step outputs, emit lazily, run the completion to flush a stateful
  xform. eduction builds on it (lazy, no longer an eager vector).
- mapcat and (apply concat coll-of-colls) hung over an infinite source because
  jolt-apply seq->lists the trailing arg and mapcat seq->lists the map result.
  Added lazy-concat-seq (lazily flatten a seq of colls); mapcat uses it directly,
  and apply special-cases concat (its result is lazy) to route through it.

Docs: a cross-cutting return-type + laziness contract in docs/spec/09-core-library;
SPEC.md notes that = masks type/laziness so they need predicate / over-infinite
rows. EBNF is reader syntax only — unaffected.

Seed change (partition-all/replace/eduction are clojure.core overlay) -> re-mint;
selfhost holds. make test + shakesmoke + buildsmoke green, 0 new divergences.
2026-06-25 22:58:50 -04:00

7.9 KiB
Raw Blame History

§9 The Core Library

Status: entry format fixed; exemplars for first, reduce, parse-uuid. The full portable surface (≈500 vars after classification, dashboard in coverage.md) is filled in chapter-by-chapter using this format.

Entries specify behavioral contracts, not implementations. Performance characteristics are specified only where the language community relies on them (e.g. vector nth is "effectively constant time" — SHOULD-level).


Collection return types & laziness (cross-cutting)

Two contracts hold across the sequence library and are not restated per entry.

Return-type fidelity. A function returns the same kind of collection the reference does — value equality is not enough, since (= [0 1] '(0 1)).

  • Sequence transformations return seqs (lazy unless noted): map, filter, remove, keep, mapcat, take/drop and their -while forms, partition, partition-all, partition-by, interpose, dedupe, distinct, concat, reductions, cons, rest, sequence. The elements of partition / partition-all / partition-by are themselves seqs, not vectors.
  • The vector variants return vectors: mapv, filterv, vec, subvec, partitionv, partitionv-all, splitv-at. split-at / split-with return a 2-vector [take drop]. A transducer applied eagerly (into [], the partition-all transducer's chunks) yields vectors.
  • Type-preserving functions return the input's type: replace over a vector is a vector, over any other seqable a (lazy) seq; empty/into (empty coll) keep the collection kind; set/into #{} return sets; into {}/select-keys/zipmap/ frequencies/group-by/merge return maps (group-by values are vectors).

Laziness. The lazy sequence functions — including sequence, eduction, and mapcat — MUST consume their source incrementally and so terminate on an infinite or unbounded source when only a prefix is demanded: (first (sequence (map inc) (range))) and (take n (mapcat f (range))) return without realizing the whole source. (apply concat coll-of-colls) is likewise lazy in its argument seq. The eager consumers (reduce, into, count, vec, doall) realize the demanded portion fully.

These are exercised by the seq / lazy over infinite and the per-fn type-predicate rows in the conformance corpus.


first — since 1.0

(first coll)

Semantics

  • S1. MUST return the first element of (seq coll).
  • S2. If (seq coll) is nil (i.e. coll is empty or nil), MUST return nil.
  • S3. MUST accept anything seqable (§5): seqs, lists, vectors, maps (yielding map entries), sets, strings (yielding characters), nil.
  • S4. On a lazy sequence, MUST realize at most the first element (§5 laziness contract).

Edge cases

  • E1. (first nil)nil; (first [])nil; (first "")nil.
  • E2. A nil or false first element is returned as-is — callers cannot distinguish "empty" from "first element is nil" via first alone (that is what seq is for).
  • E3. On a map, the element is a map entry; on an unordered collection (map, set) which element is first is implementation-defined but MUST be consistent with that collection's seq order for the same collection value.

Errors

  • X1. A non-seqable argument (e.g. a number) MUST throw.

Examples

(first [1 2 3])      ;=> 1
(first '())          ;=> nil
(first "ab")         ;=> \a
(first {:a 1})       ;=> [:a 1]
(first [nil 2])      ;=> nil

Conformance

S1S3, E1E2 → jolt sequences-spec "seq / access"; clojure-test-suite core_test/first.cljc. S4 → jolt lazy-seqs-spec counter cases. X1 → clojure-test-suite core_test/first.cljc (throwing cases).


reduce — since 1.0

(reduce f coll)
(reduce f init coll)

Semantics

  • S1. With init: MUST return init if (seq coll) is nil; otherwise MUST return (f … (f (f init e₁) e₂) … eₙ), applying f left-to-right over the elements, exactly once each.
  • S2. Without init: if coll is empty, MUST return (f) (f called with no arguments); if coll has one element, MUST return that element without calling f; otherwise as S1 with init = e₁ over e₂…eₙ.
  • S3. Reduced short-circuit: if any intermediate result is a reduced value, iteration MUST stop and the dereferenced value MUST be returned immediately; f MUST NOT be called again.
  • S4. reduce is eager: it MUST fully realize the consumed portion of a lazy coll (to the end, or to the reduced point).

Edge cases

  • E1. (reduce f nil)(f); (reduce f init nil)init.
  • E2. A reduced value as the initial init is NOT unwrapped before the first call in the reference — ⚠ under-documented; differential result to pin down and test before this entry is marked verified.
  • E3. Visit order over maps is entry order of the map's seq; over vectors/lists/seqs it is sequential order (normative).

Errors

  • X1. Without init, on an empty coll, if f has no zero-arg arity the call (f) MUST throw (arity error).

Examples

(reduce + [1 2 3 4])                                ;=> 10
(reduce + 10 [1 2 3 4])                             ;=> 20
(reduce + [])                                       ;=> 0    ; (+) is 0
(reduce + [5])                                      ;=> 5    ; f not called
(reduce (fn [a x] (if (> a 2) (reduced a) (+ a x))) 0 [1 2 3 4 5]) ;=> 3

Conformance

S1S3, E1 → jolt sequences-spec "map filter reduce" group + transducers-spec "reduce honors reduced"; clojure-test-suite core_test/reduce.cljc. S2 (single-element, f-not-called) → jolt conformance "reduce single no init". E2 → UNVERIFIED (differential test to add). S4 → lazy-seqs-spec.


parse-uuid — since 1.11

(parse-uuid s)

Semantics

  • S1. If s is a string in canonical UUID form — five groups of hex digits of lengths 8, 4, 4, 4, 12 separated by - — MUST return a UUID value u such that (uuid? u) is true and (str u) is the lowercase form of s.
  • S2. Parsing MUST be case-insensitive and equality on the results case-insensitive: (= (parse-uuid s) (parse-uuid (upper-case s))) is true.
  • S3. If s is a string not in canonical form, MUST return nil. ⚠ reference-divergence: reference Clojure (java.util.UUID) additionally accepts non-canonical forms like "0-0-0-0-0"; ClojureScript and other dialects are strict. This spec adopts strict (the cross-dialect behavior); the reference's permissiveness is recorded as host leniency.
  • S4. UUID values MUST support value equality, hashing (usable as map keys and set members), str (lowercase canonical form), and print as the tagged literal #uuid "…" such that the printed form reads back equal (§2 tagged literals).

Edge cases

  • E1. "", over-long, truncated, non-hex characters, and misplaced dashes ⇒ nil.

Errors

  • X1. A non-string argument MUST throw.

Examples

(parse-uuid "b6883c0a-0342-4007-9966-bc2dfa6b109e")  ;=> #uuid "b6883c0a-…"
(uuid? *1)                                            ;=> true
(parse-uuid "df0993")                                 ;=> nil
(parse-uuid 1000)                                     ;; throws

Conformance

S1S4, E1, X1 → jolt uuid-spec (30 cases) + 6 three-path conformance cases; clojure-test-suite core_test/parse_uuid.cljc, core_test/uuid_qmark.cljc, core_test/random_uuid.cljc.


Authoring notes

  • Source examples from the ClojureDocs export (clojuredocs-export.edn, 648 core vars have community examples) — but every example is verified against the reference before inclusion.
  • When writing an entry surfaces a behavior question, settle it by differential test first; if dialects split, that's a classification decision (host-dependent / divergence note), not a coin flip.
  • An entry is Verified when no field carries UNVERIFIED; coverage.md tracks per-var status.