on master help sign in
Document trait hierarchy, Sequence/Traversable, Cons patterns, and add comprehensive stdlib function signatures
commit6590a26d7a066b1a9d400c82e8d16ff7260cacbd authoriko <mail@iko.soy> dateMon 11 May 2026, 15:08 (5 months ago) parent99a59ae
5 files changed+524−70
DESIGN.md+15−4 README.md+4 base.md+175−22 knot.md+99−18 stdlib.md+231−26
DESIGN.md+15−4
@@ -245,8 +245,17 @@ trait Applicative f => Alternative (f : Type -> Type) where
245245
246246trait Foldable (t : Type -> Type) where
247247 fold : (b -> a -> b) -> b -> t a -> b
248+
249+trait Foldable t => Traversable (t : Type -> Type) where
250+ traverse : (a -> f b) -> t a -> f (t b)
251+
252+trait Sequence s where
253+ take : Int -> s -> s
254+ drop : Int -> s -> s
248255```
249256
257+`Sequence` has built-in impls for both `Text` (character take/drop) and relations (row take/drop), so the same `take 5 x` works on a string or a relation.
258+
250259### `do` Desugaring
251260
252261`do` syntax works for any `Monad`. Do blocks can appear anywhere an expression is expected, including as function arguments: `f do ...` or `f (do ...)`.
@@ -497,11 +506,13 @@ scale = \factor -> do
497506
498507```knot
499508describe = \rel -> case rel of
500− [] -> "empty"
501− [{name: n}] -> "just " ++ n
502− _ -> show (count rel) ++ " rows"
509+ [] -> "empty"
510+ [{name: n}] -> "just " ++ n
511+ Cons h _ -> "first of many: " ++ show h
503512```
504513
514+`[]` matches an empty relation. `[p1, p2, ...]` matches a relation with exactly that many rows in any iteration order. `Cons head tail` matches a non-empty relation, binding `head` to the first row and `tail` to the rest (the relation has no inherent order; `Cons` chooses a deterministic iteration order for the match).
515+
505516### Grouping
506517
507518`groupBy` partitions a relation by key fields, like SQL's `GROUP BY`. After `groupBy`, the bound variable becomes a sub-relation (the group), enabling aggregation:
@@ -873,7 +884,7 @@ api = serve Api where
873884 users <- *people
874885 case filter (\u -> u.id == id) users of
875886 [] -> yield Err {error: {status: 404, message: "user not found"}}
876− [u | _] -> yield Ok {value: u}
887+ Cons u _ -> yield Ok {value: u}
877888 CreateUser = \{name, email} -> do
878889 if length name == 0 then
879890 yield Err {error: {status: 400, message: "name required"}}
README.md+4
@@ -52,6 +52,10 @@ main = do
5252 yield {}
5353```
5454
55+See [examples/](examples/) for more — HTTP routes, refined types, STM
56+transactions, concurrent `fork`/`race`, units of measure, custom monads,
57+and more.
58+
5559## Tests
5660
5761```sh
base.md+175−22
@@ -110,12 +110,49 @@ fold (\acc p -> acc ++ p.name ++ " ") "" *people
110110```
111111
112112```knot
113−single : [a] -> a
113+single : [a] -> Maybe a
114114```
115−Extract the single element from a one-element relation. Panics if the relation is empty or has more than one element.
115+Extract the single element from a one-element relation. Returns `Just {value: x}` for a singleton, `Nothing {}` otherwise.
116116
117117```knot
118−single [{name: "Alice"}] -- {name: "Alice"}
118+single [{name: "Alice"}] -- Just {value: {name: "Alice"}}
119+single [] -- Nothing {}
120+single [1, 2] -- Nothing {}
121+```
122+
123+```knot
124+head : [a] -> Maybe a
125+```
126+First row of a relation in iteration order, or `Nothing {}` if empty.
127+
128+```knot
129+findFirst : [a] -> (a -> Bool) -> Maybe a
130+```
131+First row matching the predicate, or `Nothing {}` if none match. Lazy — stops at the first match.
132+
133+```knot
134+findFirst [1, 2, 3, 4, 5] (\x -> x > 3) -- Just {value: 4}
135+findFirst [1, 2, 3] (\x -> x > 10) -- Nothing {}
136+```
137+
138+```knot
139+any : (a -> Bool) -> [a] -> Bool
140+all : (a -> Bool) -> [a] -> Bool
141+```
142+`any` returns `True` if any row satisfies the predicate; `all` returns `True` only when every row does (vacuously `True` on empty).
143+
144+```knot
145+elem : a -> [a] -> Bool
146+```
147+Membership check by structural equality.
148+
149+```knot
150+sortBy : (a -> b) -> [a] -> [a]
151+```
152+Reorder rows by a projected key. The key type `b` must be `Ord`. Returns a relation with the rows in ascending key order. Pushes down to SQL `ORDER BY` when applied to a source relation; combined with `take` it becomes `ORDER BY ... LIMIT`.
153+
154+```knot
155+*employees |> sortBy (\e -> e.salary) |> take 5
119156```
120157
121158```knot
@@ -209,21 +246,23 @@ toLower : Text -> Text
209246Convert text to lowercase.
210247
211248```knot
212−take : Int -> Text -> Text
249+take : Int<u> -> s -> s -- Sequence trait method
213250```
214−First *n* characters of a text value.
251+First *n* elements. `Sequence` has built-in impls for `Text` (characters) and `[]` (rows).
215252
216253```knot
217−take 5 "hello world" -- "hello"
254+take 5 "hello world" -- "hello"
255+take 2 [10, 20, 30, 40] -- [10, 20]
218256```
219257
220258```knot
221−drop : Int -> Text -> Text
259+drop : Int<u> -> s -> s -- Sequence trait method
222260```
223−Skip the first *n* characters.
261+Skip the first *n* elements.
224262
225263```knot
226−drop 6 "hello world" -- "world"
264+drop 6 "hello world" -- "world"
265+drop 1 [10, 20, 30] -- [20, 30]
227266```
228267
229268```knot
@@ -260,6 +299,61 @@ Convert text into a relation of single-character text values.
260299chars "abc" -- ["a", "b", "c"]
261300```
262301
302+## Logging
303+
304+The leveled logging functions write to stderr (so they don't mix with regular
305+`println` output). When stderr is a TTY, output is colored; otherwise it's
306+emitted as one JSON record per line, suitable for log aggregation.
307+
308+```knot
309+logInfo : a -> IO {console} {}
310+logWarn : a -> IO {console} {}
311+logError : a -> IO {console} {}
312+logDebug : a -> IO {console} {}
313+```
314+
315+`logDebug` is gated on a `--debug` runtime flag — debug records are dropped
316+silently when the program is launched without it.
317+
318+```knot
319+main = do
320+ logInfo "starting"
321+ logWarn {event: "low memory", availableMb: 64}
322+ yield {}
323+```
324+
325+## Control Flow
326+
327+```knot
328+when : Bool -> IO r {} -> IO r {}
329+unless : Bool -> IO r {} -> IO r {}
330+```
331+
332+Run an IO action when the condition is `True`/`False` respectively. Both
333+return `yield {}` when the action is skipped, so the effect row of the result
334+is the row of the supplied action.
335+
336+```knot
337+main = do
338+ when (n > 0) (println "positive")
339+ unless verbose do
340+ println "(quiet mode)"
341+ yield {}
342+```
343+
344+```knot
345+forEach : [a] -> (a -> IO r {}) -> IO r {}
346+```
347+
348+Iterate a relation and sequence an IO action for each row. Effect row `r`
349+propagates from the action.
350+
351+```knot
352+main = do
353+ forEach ["a", "b", "c"] (\s -> println s)
354+ yield {}
355+```
356+
263357## Utility
264358
265359```knot
@@ -272,6 +366,17 @@ not : Bool -> Bool
272366```
273367Boolean negation. Function form of the `!` operator.
274368
369+```knot
370+stripUnit : Int<u> -> Int
371+withUnit : Int -> Int<u>
372+stripFloatUnit : Float<u> -> Float
373+withFloatUnit : Float -> Float<u>
374+```
375+
376+Drop or attach a unit tag on `Int`/`Float`. All four are identity at
377+runtime — they exist only for the type checker. Use them to rebrand a value
378+when you need a different concrete unit (e.g. `Ms` → `S`).
379+
275380```knot
276381now : IO {clock} Int<Ms>
277382```
@@ -340,12 +445,12 @@ parseJson json -- {name: "Bob"}
340445## Bytes
341446
342447```knot
343−bytesLength : Bytes -> Int
448+bytesLength : Bytes -> Int<u>
344449```
345450Number of bytes in a byte string.
346451
347452```knot
348−bytesSlice : Int -> Int -> Bytes -> Bytes
453+bytesSlice : Int<u1> -> Int<u2> -> Bytes -> Bytes
349454```
350455Extract a sub-range. `bytesSlice start len bytes` returns `len` bytes starting at offset `start`.
351456
@@ -355,7 +460,7 @@ bytesConcat : Bytes -> Bytes -> Bytes
355460Concatenate two byte strings.
356461
357462```knot
358−bytesGet : Int -> Bytes -> Int
463+bytesGet : Int<u1> -> Bytes -> Int<u2>
359464```
360465Get the byte value (0–255) at the given index.
361466
@@ -365,23 +470,34 @@ textToBytes : Text -> Bytes
365470UTF-8 encode a text value into bytes.
366471
367472```knot
368−bytesToText : Bytes -> Text
473+bytesToText : Bytes -> Maybe Text
369474```
370−UTF-8 decode bytes into text.
475+UTF-8 decode bytes into text. Returns `Nothing {}` on invalid UTF-8 (use `case` to handle both branches).
371476
372477```knot
373478bytesToHex : Bytes -> Text
374479```
375−Encode bytes as a hexadecimal text string.
480+Encode bytes as a hexadecimal text string. Always succeeds.
376481
377482```knot
378−bytesFromHex : Text -> Bytes
483+bytesFromHex : Text -> Maybe Bytes
379484```
380−Decode a hexadecimal text string into bytes.
485+Decode a hexadecimal text string into bytes. Returns `Nothing {}` on odd-length, non-hex, or non-ASCII input. `hexDecode` is an alias.
381486
382487```knot
383488bytesToHex (textToBytes "hi") -- "6869"
384−bytesFromHex "6869" |> bytesToText -- "hi"
489+case bytesFromHex "6869" of
490+ Just {value: b} -> bytesToText b -- Just {value: "hi"}
491+ Nothing {} -> Nothing {}
492+```
493+
494+```knot
495+hash : a -> Bytes
496+```
497+SHA-256 hash of any value, returned as 32 bytes. `Bytes` and `Text` hash their raw contents; records, relations, and constructors hash a canonical serialisation, so the same logical value always produces the same digest.
498+
499+```knot
500+bytesToHex (hash "hello") -- "2cf24dba..."
385501```
386502
387503## File System
@@ -453,12 +569,19 @@ main = do
453569## Server
454570
455571```knot
456−listen : Int -> Server a -> IO {network} {}
572+listen : Int<u> -> Server a r -> IO {network | r} {}
573+listenOn : Text -> Int<u> -> Server a r -> IO {network | r} {}
457574```
458−Start an HTTP server on the given port. The second argument is a `Server a` value, typically built with the `serve API where ...` expression. Has the `{network}` effect. See `route` declarations in the language spec for defining typed endpoints and the `serve` form for binding handlers.
575+Start an HTTP server. `listen` binds to all interfaces; `listenOn` takes an explicit bind address. The `Server a r` value is built with the `serve API where ...` expression, and its row variable `r` unifies with `listen`'s IO row — so handler effects (e.g. `console` from a handler that calls `println`) propagate into the program's IO type.
459576
460577Handlers return `Result HttpError T` where `HttpError = {status: Int, message: Text}` and `T` is the response type declared on the route. `Ok {value: T}` responds with HTTP 200; `Err {error: {status, message}}` responds with the given status code (clamped to 100..=599) and a JSON `{"error": message}` body — use this for application-level errors like 404 not found or 401 unauthorized.
461578
579+```knot
580+fetch : Text -> Endpoint -> IO {network} (Result HttpError T)
581+fetchWith : Text -> {headers: [{name: Text, value: Text}]} -> Endpoint -> IO {network} (Result HttpError T)
582+```
583+Type-safe HTTP client. `Endpoint` is a route constructor (e.g. `GetTodos {owner: "alice"}`) — the response type `T` is inferred from the route declaration. `fetchWith` merges ad-hoc headers with route-declared headers. When response headers are declared on the route, the success body is wrapped as `{body: T, headers: H}` inside `Ok`.
584+
462585### Rate limiting
463586
464587Endpoints can declare a per-route token-bucket rate limit with the `rateLimit <expr>` clause (placed after the response type/headers, before `=`). The expression has type `RateLimit input a = {key: input -> RequestCtx -> Maybe a, limit: {requests: Int, window: Int<Ms>}}` with `Ord a`. `input` is the same record the handler receives (path/query/body/header fields). `RequestCtx = {clientIp: Text, receivedAt: Int<Ms>, header: Text -> Maybe Text}` is supplied by the runtime; `header` does case-insensitive lookup.
@@ -578,10 +701,12 @@ Structural equality as a trait method. Built-in impls for `Int`, `Float`, `Text`
578701### Ord
579702
580703```knot
704+data Ordering = LT {} | EQ {} | GT {}
705+
581706trait Eq a => Ord a where
582− compare : a -> a -> Int
707+ compare : a -> a -> Ordering
583708```
584−Ordering comparison. Returns `-1`, `0`, or `1`. Requires `Eq`. Built-in impls for `Int`, `Float`, `Text`.
709+Ordering comparison. Returns `LT {}`, `EQ {}`, or `GT {}`. Requires `Eq`. Built-in impls for `Int`, `Float`, `Text`.
585710
586711### Num
587712
@@ -671,6 +796,15 @@ trait Applicative f => Alternative (f : Type -> Type) where
671796```
672797Built-in impls: `[]` (`empty = []`, `alt = union`) and `Maybe` (`empty = Nothing {}`, `alt` takes the first `Just`).
673798
799+### Sequence
800+
801+```knot
802+trait Sequence s where
803+ take : Int -> s -> s
804+ drop : Int -> s -> s
805+```
806+Built-in impls for `Text` (operates on characters) and `[]` (operates on rows). When the receiver is a source relation, the compiler pushes `take` down to SQL `LIMIT`; combined with `sortBy` it becomes `ORDER BY ... LIMIT`.
807+
674808### Foldable
675809
676810```knot
@@ -678,6 +812,25 @@ trait Foldable (t : Type -> Type) where
678812 fold : (b -> a -> b) -> b -> t a -> b
679813```
680814
815+### Traversable
816+
817+```knot
818+trait Foldable t => Traversable (t : Type -> Type) where
819+ traverse : (a -> f b) -> t a -> f (t b)
820+```
821+Walk a structure left-to-right, sequencing through any `Applicative` `f`. Built-in impl for `[]` over `Maybe` (`Nothing` short-circuits) — useful for validating every row and collecting the result or first failure.
822+
823+### ToJSON / FromJSON
824+
825+```knot
826+trait ToJSON a where
827+ toJson : a -> Text
828+
829+trait FromJSON a where
830+ parseJson : Text -> a
831+```
832+JSON encode/decode as trait methods. Built-in instances cover records, relations, primitives, ADTs, and `Maybe`/`Result`/`Bool`.
833+
681834## Operators
682835
683836These are not functions but are available as infix operators:
knot.md+99−18
@@ -348,7 +348,21 @@ priority = \p -> case p of
348348describe = \rel -> case rel of
349349 [] -> "empty"
350350 [{name: n}] -> "just " ++ n
351− _ -> show (count rel) ++ " rows"
351+ Cons h _ -> "first of many: " ++ show h
352+```
353+
354+| Pattern | Matches |
355+|---------|---------|
356+| `[]` | empty relation |
357+| `[p1, p2, ...]` | relation with exactly that many rows |
358+| `Cons head tail` | non-empty relation, `head` is one row, `tail` is the rest |
359+
360+`Cons` is also useful in recursive functions:
361+
362+```knot
363+sumList = \xs -> case xs of
364+ [] -> 0
365+ Cons h t -> h + sumList t
352366```
353367
354368---
@@ -518,10 +532,13 @@ sortAndShow : Ord a => Display a => [a] -> [Text]
518532```
519533Eq -- ==, !=
520534├── Ord -- <, >, <=, >= (returns Ordering: LT {} | EQ {} | GT {})
521−└── Num -- +, -, *, /, unary -
535+└── Num -- +, -, *, /, %, unary -
522536
523537Semigroup -- ++ (text concat, relation concat)
524538Display -- display : a -> Text
539+ToJSON -- toJson : a -> Text
540+FromJSON -- parseJson : Text -> a
541+Sequence -- take, drop (impls for Text and [a])
525542
526543Functor (f : Type -> Type) -- map
527544└── Applicative (f : Type -> Type) -- yield, ap
@@ -529,8 +546,14 @@ Functor (f : Type -> Type) -- map
529546 └── Alternative (f : Type -> Type) -- empty, alt (enables where)
530547
531548Foldable (t : Type -> Type) -- fold
549+└── Traversable (t : Type -> Type) -- traverse
532550```
533551
552+Built-in impls of the Functor/Applicative/Monad/Alternative hierarchy ship
553+for `[]`, `Maybe`, `Result`, and `IO`, so do-notation works on all of them.
554+`Sequence` ships with impls for `Text` and `[]`, so `take 5 x` works on a
555+string or a relation.
556+
534557---
535558
536559## IO and Effects
@@ -543,7 +566,7 @@ Effectful functions return `IO {effects} a` — descriptions of effects, not imm
543566-- External effects
544567println : a -> IO {console} {}
545568readFile : Text -> IO {fs} Text
546−now : IO {clock} Int
569+now : IO {clock} Int<Ms>
547570
548571-- DB operations (empty effect set)
549572-- *rel : IO {} [T]
@@ -554,10 +577,10 @@ now : IO {clock} Int
554577
555578| Effect | Functions |
556579|--------|-----------|
557−| `console` | `println`, `print`, `readLine` |
580+| `console` | `println`, `print`, `readLine`, `logInfo`, `logWarn`, `logError`, `logDebug` |
558581| `fs` | `readFile`, `writeFile`, `appendFile`, `fileExists`, `removeFile`, `listDir` |
559582| `clock` | `now`, `sleep` |
560−| `random` | `randomInt`, `randomFloat`, `randomUuid` |
583+| `random` | `randomInt`, `randomFloat`, `randomUuid`, `generateKeyPair`, `generateSigningKeyPair`, `encrypt` |
561584| `network` | `listen`, `fetch`, `fetchWith` |
562585
563586### IO Do Blocks
@@ -609,7 +632,7 @@ The body of `atomic` must be an IO expression containing only DB operations. Ext
609632Fire-and-forget: runs an IO action on a new OS thread. Each thread gets its own SQLite connection (WAL mode).
610633
611634```knot
612−fork : IO {} {} -> IO {} {}
635+fork : IO r {} -> IO {} {}
613636
614637main = do
615638 fork do
@@ -620,7 +643,9 @@ main = do
620643 -- main waits for all spawned threads before exiting
621644```
622645
623−Do blocks can be passed directly as arguments: `fork do ...` (no parentheses needed).
646+The spawned action's effect row `r` is unconstrained — `fork` can spawn any
647+IO — but those effects do not propagate back to the caller. Do blocks can be
648+passed directly as arguments: `fork do ...` (no parentheses needed).
624649
625650#### `retry`
626651
@@ -703,21 +728,30 @@ and the loser unwinds at its next safe point.
703728| Function | Type | Description |
704729|----------|------|-------------|
705730| `filter` | `(a -> Bool) -> [a] -> [a]` | Keep matching rows |
706−| `map` | `(a -> b) -> [a] -> [b]` | Transform each row |
731+| `map` | `(a -> b) -> [a] -> [b]` | Transform each row (`Functor.map`) |
707732| `match` | `Constructor -> [ADT] -> [Payload]` | Filter by variant |
708−| `fold` | `(b -> a -> b) -> b -> [a] -> b` | Left fold |
709−| `count` | `[a] -> Int` | Number of rows |
710−| `countWhere` | `(a -> Bool) -> [a] -> Int` | Filtered count (SQL-pushed when possible) |
733+| `fold` | `(b -> a -> b) -> b -> [a] -> b` | Left fold (`Foldable.fold`) |
734+| `count` | `[a] -> Int<u>` | Number of rows |
735+| `countWhere` | `(a -> Bool) -> [a] -> Int<u>` | Filtered count (SQL-pushed when possible) |
711736| `sum` | `(a -> b) -> [a] -> b` | Sum projected field (preserves units) |
712737| `avg` | `(a -> Float<u>) -> [a] -> Float<u>` | Average projected field (preserves units) |
713738| `minOn` | `(a -> b) -> [a] -> b` | Min of projected field (panics on empty) |
714739| `maxOn` | `(a -> b) -> [a] -> b` | Max of projected field (panics on empty) |
715740| `min` | `Ord a => a -> a -> a` | Binary min of two values |
716741| `max` | `Ord a => a -> a -> a` | Binary max of two values |
717−| `single` | `[a] -> Maybe a` | Extract singleton |
742+| `head` | `[a] -> Maybe a` | First row, or `Nothing` if empty |
743+| `findFirst` | `[a] -> (a -> Bool) -> Maybe a` | First row matching predicate |
744+| `single` | `[a] -> Maybe a` | `Just x` for a singleton, `Nothing` otherwise |
745+| `any` | `(a -> Bool) -> [a] -> Bool` | True if any row matches |
746+| `all` | `(a -> Bool) -> [a] -> Bool` | True if every row matches |
747+| `elem` | `a -> [a] -> Bool` | Membership by structural equality |
718748| `union` | `[a] -> [a] -> [a]` | Set union |
719749| `diff` | `[a] -> [a] -> [a]` | Set difference |
720750| `inter` | `[a] -> [a] -> [a]` | Set intersection |
751+| `sortBy` | `(a -> b) -> [a] -> [a]` | Reorder rows by projected key (`Ord b`) |
752+| `take` | `Int<u> -> [a] -> [a]` | First *n* rows (`Sequence.take`) |
753+| `drop` | `Int<u> -> [a] -> [a]` | Drop first *n* rows (`Sequence.drop`) |
754+| `reverse` | `[a] -> [a]` | Reverse iteration order |
721755
722756### Text
723757
@@ -725,21 +759,29 @@ and the loser unwinds at its next safe point.
725759|----------|------|-------------|
726760| `toUpper` | `Text -> Text` | Uppercase |
727761| `toLower` | `Text -> Text` | Lowercase |
728−| `length` | `Text -> Int` | Character count |
762+| `length` | `Text -> Int<u>` | Character count |
729763| `trim` | `Text -> Text` | Strip whitespace |
730764| `reverse` | `Text -> Text` | Reverse |
731765| `chars` | `Text -> [Text]` | Split to characters |
732−| `take` | `Int -> Text -> Text` | First n characters |
733−| `drop` | `Int -> Text -> Text` | Drop first n characters |
766+| `take` | `Int<u> -> Text -> Text` | First *n* characters (`Sequence.take`) |
767+| `drop` | `Int<u> -> Text -> Text` | Drop first *n* characters (`Sequence.drop`) |
734768| `contains` | `Text -> Text -> Bool` | Substring check |
735769
770+`take` and `drop` are `Sequence` trait methods with built-in impls for both
771+`Text` and relations.
772+
736773### Conversion
737774
738775| Function | Type | Description |
739776|----------|------|-------------|
740777| `show` | `a -> Text` | Any value to text |
741−| `toJson` | `a -> Text` | Encode as JSON |
742−| `parseJson` | `Text -> a` | Decode JSON |
778+| `toJson` | `a -> Text` | Encode as JSON (`ToJSON.toJson`) |
779+| `parseJson` | `Text -> a` | Decode JSON (`FromJSON.parseJson`) |
780+| `display` | `Display a => a -> Text` | Render a value via `Display` |
781+| `stripUnit` | `Int<u> -> Int` | Drop unit tag from `Int` |
782+| `withUnit` | `Int -> Int<u>` | Attach unit tag to `Int` |
783+| `stripFloatUnit` | `Float<u> -> Float` | Drop unit tag from `Float` |
784+| `withFloatUnit` | `Float -> Float<u>` | Attach unit tag to `Float` |
743785
744786### IO
745787
@@ -748,6 +790,10 @@ and the loser unwinds at its next safe point.
748790| `println` | `a -> IO {console} {}` | Print with newline |
749791| `print` | `a -> IO {console} {}` | Print without newline |
750792| `readLine` | `IO {console} Text` | Read stdin line |
793+| `logInfo` | `a -> IO {console} {}` | Leveled log to stderr (INFO) |
794+| `logWarn` | `a -> IO {console} {}` | Leveled log to stderr (WARN) |
795+| `logError` | `a -> IO {console} {}` | Leveled log to stderr (ERROR) |
796+| `logDebug` | `a -> IO {console} {}` | DEBUG; only emits when run with `--debug` |
751797| `readFile` | `Text -> IO {fs} Text` | Read file |
752798| `writeFile` | `Text -> Text -> IO {fs} {}` | Write file (path, content) |
753799| `appendFile` | `Text -> Text -> IO {fs} {}` | Append to file |
@@ -763,6 +809,41 @@ and the loser unwinds at its next safe point.
763809| `fork` | `IO r {} -> IO {} {}` | Fire-and-forget on new OS thread |
764810| `race` | `IO r a -> IO r b -> IO r (Result a b)` | Run two IO actions, return the winner |
765811| `retry` | `a` | Rollback and wait (inside `atomic` only) |
812+| `when` | `Bool -> IO r {} -> IO r {}` | Run action when condition is true |
813+| `unless` | `Bool -> IO r {} -> IO r {}` | Run action when condition is false |
814+| `forEach` | `[a] -> (a -> IO r {}) -> IO r {}` | Sequence an action over each row |
815+| `listen` | `Int<u> -> Server a r -> IO {network \| r} {}` | Start an HTTP server |
816+| `fetch` | `Text -> Endpoint -> IO {network} (Result HttpError T)` | Type-safe HTTP client |
817+| `fetchWith` | `Text -> {headers: [..]} -> Endpoint -> IO {network} (Result HttpError T)` | `fetch` with ad-hoc headers |
818+
819+`listen`'s effect row unifies with the `Server`'s, so handler effects (e.g.
820+`console` from a handler that calls `println`) flow into the IO type of the
821+program.
822+
823+### Bytes
824+
825+| Function | Type | Description |
826+|----------|------|-------------|
827+| `textToBytes` | `Text -> Bytes` | UTF-8 encode |
828+| `bytesToText` | `Bytes -> Maybe Text` | UTF-8 decode (`Nothing` on invalid UTF-8) |
829+| `bytesLength` | `Bytes -> Int<u>` | Byte length |
830+| `bytesSlice` | `Int<u1> -> Int<u2> -> Bytes -> Bytes` | Sub-range (start, length, bytes) |
831+| `bytesConcat` | `Bytes -> Bytes -> Bytes` | Concatenate |
832+| `bytesGet` | `Int<u1> -> Bytes -> Int<u2>` | Byte value (0–255) at index |
833+| `bytesToHex` | `Bytes -> Text` | Hex encode |
834+| `bytesFromHex` | `Text -> Maybe Bytes` | Hex decode (`Nothing` on bad input) |
835+| `hash` | `a -> Bytes` | SHA-256 hash (32 bytes) of any value |
836+
837+### Cryptography
838+
839+| Function | Type | Description |
840+|----------|------|-------------|
841+| `generateKeyPair` | `IO {random} {privateKey: Bytes, publicKey: Bytes}` | X25519 keypair |
842+| `generateSigningKeyPair` | `IO {random} {privateKey: Bytes, publicKey: Bytes}` | Ed25519 keypair |
843+| `encrypt` | `Bytes -> Bytes -> IO {random} Bytes` | Sealed-box (public key, plaintext) |
844+| `decrypt` | `Bytes -> Bytes -> Bytes` | Open sealed-box (private key, ciphertext) |
845+| `sign` | `Bytes -> Bytes -> Bytes` | Ed25519 sign (private key, message) |
846+| `verify` | `Bytes -> Bytes -> Bytes -> Bool` | Verify (public key, message, signature) |
766847
767848### Utility
768849
@@ -825,7 +906,7 @@ api = serve Api where
825906 users <- *people
826907 case filter (\u -> u.id == id) users of
827908 [] -> yield Err {error: {status: 404, message: "user not found"}}
828− [u | _] -> yield Ok {value: u}
909+ Cons u _ -> yield Ok {value: u}
829910```
830911
831912Status is clamped to `100..=599`. The runtime emits `400` for path/query/body parsing failures and refinement violations, and `404` for unmatched routes — only return `Err` for application-level errors.
stdlib.md+231−26
@@ -8,11 +8,13 @@ Complete reference for all built-in functions, traits, and types.
88- [Concurrency](#concurrency)
99- [Text Operations](#text-operations)
1010- [Console I/O](#console-io)
11+- [Control Flow](#control-flow)
1112- [File System](#file-system)
1213- [Time](#time)
1314- [Random](#random)
1415- [JSON](#json)
1516- [Bytes](#bytes)
17+- [HTTP](#http)
1618- [Cryptography](#cryptography)
1719- [Utility Functions](#utility-functions)
1820- [Built-in Traits](#built-in-traits)
@@ -80,7 +82,13 @@ totalAmount = \rel -> fold (\acc r -> acc + r.amount) 0 rel
8082single : [a] -> Maybe a
8183```
8284
83−Extract the single element of a relation. Returns `Just {value: x}` for a singleton, `Nothing {}` otherwise.
85+Extract the single element of a relation. Returns `Just {value: x}` for a singleton, `Nothing {}` for empty or multi-element relations.
86+
87+```knot
88+single [{name: "Alice"}] -- Just {value: {name: "Alice"}}
89+single [] -- Nothing {}
90+single [1, 2] -- Nothing {}
91+```
8492
8593### `count`
8694
@@ -214,6 +222,77 @@ inter : [a] -> [a] -> [a]
214222
215223Set intersection — rows present in both relations.
216224
225+### `head`
226+
227+```
228+head : [a] -> Maybe a
229+```
230+
231+First row of a relation in iteration order, or `Nothing {}` if empty.
232+
233+### `findFirst`
234+
235+```
236+findFirst : [a] -> (a -> Bool) -> Maybe a
237+```
238+
239+First row matching the predicate (left-to-right), or `Nothing {}` when no row matches. Stops at the first hit.
240+
241+```knot
242+findFirst [1, 2, 3, 4, 5] (\x -> x > 3) -- Just {value: 4}
243+```
244+
245+### `any` / `all`
246+
247+```
248+any : (a -> Bool) -> [a] -> Bool
249+all : (a -> Bool) -> [a] -> Bool
250+```
251+
252+`any` is `True` when some row matches; `all` is `True` only when every row matches (vacuously `True` on `[]`).
253+
254+### `elem`
255+
256+```
257+elem : a -> [a] -> Bool
258+```
259+
260+Membership check by structural equality.
261+
262+### `sortBy`
263+
264+```
265+sortBy : (a -> b) -> [a] -> [a]
266+```
267+
268+Reorder rows by a projected key. The key type `b` must be `Ord`. Returns a new relation with rows in ascending key order. Sets have no inherent order; the result preserves the sorted order for downstream iteration (`fold`, `map`, `forEach`, etc.).
269+
270+Pushes down to SQL `ORDER BY` when applied to a source relation. Combined with `take` it becomes `ORDER BY ... LIMIT`:
271+
272+```knot
273+&topFive = do
274+ employees <- *employees
275+ yield (employees |> sortBy (\e -> -e.salary) |> take 5)
276+-- SQL: SELECT ... FROM _knot_employees ORDER BY -salary LIMIT 5
277+```
278+
279+### `take` / `drop`
280+
281+```
282+take : Int -> [a] -> [a] -- Sequence.take
283+drop : Int -> [a] -> [a] -- Sequence.drop
284+```
285+
286+First / drop *n* rows. Both come from the `Sequence` trait, which also has a `Text` impl that operates on characters.
287+
288+### `reverse`
289+
290+```
291+reverse : [a] -> [a]
292+```
293+
294+Reverse iteration order of a relation. Like `sortBy`, this controls iteration order; the underlying set is unchanged.
295+
217296---
218297
219298## Concurrency
@@ -370,28 +449,23 @@ chars : Text -> [Text]
370449
371450Split text into a relation of single characters.
372451
373−### `take`
374−
375−```
376−take : Int -> Text -> Text
377−```
378−
379−Return the first *n* characters.
380−
381−```knot
382−first3 = take 3 "hello" -- "hel"
383−```
452+### `take` / `drop`
384453
385−### `drop`
454+`take` and `drop` are `Sequence` trait methods with built-in impls for both
455+`Text` (characters) and relations (rows):
386456
387457```
458+take : Int -> Text -> Text -- characters
459+take : Int -> [a] -> [a] -- rows
388460drop : Int -> Text -> Text
461+drop : Int -> [a] -> [a]
389462```
390463
391−Drop the first *n* characters.
392−
393464```knot
394−rest = drop 3 "hello" -- "lo"
465+take 3 "hello" -- "hel"
466+take 2 [10, 20, 30] -- [10, 20]
467+drop 3 "hello" -- "lo"
468+drop 1 [10, 20, 30] -- [20, 30]
395469```
396470
397471### `contains`
@@ -426,6 +500,24 @@ print : a -> IO {console} {}
426500
427501Print a value to stdout without a trailing newline.
428502
503+### `logInfo` / `logWarn` / `logError` / `logDebug`
504+
505+```
506+logInfo : a -> IO {console} {}
507+logWarn : a -> IO {console} {}
508+logError : a -> IO {console} {}
509+logDebug : a -> IO {console} {}
510+```
511+
512+Leveled logging to stderr (so output does not mix with `println` on stdout). When stderr is a TTY, output is colored; otherwise each record is written as one JSON line for log aggregators. `logDebug` only emits when the program is launched with `--debug` — debug records are dropped silently otherwise.
513+
514+```knot
515+main = do
516+ logInfo "starting"
517+ logWarn {event: "low memory", availableMb: 64}
518+ yield {}
519+```
520+
429521### `show`
430522
431523```
@@ -444,6 +536,38 @@ Read a line of input from stdin.
444536
445537---
446538
539+## Control Flow
540+
541+### `when` / `unless`
542+
543+```
544+when : Bool -> IO r {} -> IO r {}
545+unless : Bool -> IO r {} -> IO r {}
546+```
547+
548+Run an IO action conditionally. `when cond a` runs `a` if `cond` is `True {}`; `unless cond a` runs `a` if `cond` is `False {}`. The skipped branch becomes `yield {}`. The action's effect row `r` propagates to the result.
549+
550+```knot
551+when (n > 0) (println "positive")
552+
553+unless verbose do
554+ println "(quiet mode)"
555+```
556+
557+### `forEach`
558+
559+```
560+forEach : [a] -> (a -> IO r {}) -> IO r {}
561+```
562+
563+Sequence an IO action over each row of a relation. Iteration follows the relation's deterministic order (after any `sortBy`).
564+
565+```knot
566+forEach ["a", "b", "c"] (\s -> println s)
567+```
568+
569+---
570+
447571## File System
448572
449573All file system functions return `IO {fs}` values.
@@ -594,15 +718,15 @@ Encode text as UTF-8 bytes.
594718### `bytesToText`
595719
596720```
597−bytesToText : Bytes -> Text
721+bytesToText : Bytes -> Maybe Text
598722```
599723
600−Decode UTF-8 bytes to text.
724+UTF-8 decode bytes to text. Returns `Nothing {}` on invalid UTF-8.
601725
602726### `bytesLength`
603727
604728```
605−bytesLength : Bytes -> Int
729+bytesLength : Bytes -> Int<u>
606730```
607731
608732Return the byte length.
@@ -613,15 +737,15 @@ Return the byte length.
613737bytesToHex : Bytes -> Text
614738```
615739
616−Encode bytes as a hexadecimal string.
740+Encode bytes as a hexadecimal string. Always succeeds.
617741
618742### `bytesFromHex`
619743
620744```
621−bytesFromHex : Text -> Bytes
745+bytesFromHex : Text -> Maybe Bytes
622746```
623747
624−Decode a hexadecimal string to bytes.
748+Decode a hexadecimal string to bytes. Returns `Nothing {}` on odd-length, non-hex, or non-ASCII input. `hexDecode` is an alias.
625749
626750### `bytesConcat`
627751
@@ -634,7 +758,7 @@ Concatenate two byte strings.
634758### `bytesGet`
635759
636760```
637−bytesGet : Int -> Bytes -> Int
761+bytesGet : Int<u1> -> Bytes -> Int<u2>
638762```
639763
640764Get the byte value (0–255) at the given index.
@@ -642,11 +766,48 @@ Get the byte value (0–255) at the given index.
642766### `bytesSlice`
643767
644768```
645−bytesSlice : Int -> Int -> Bytes -> Bytes
769+bytesSlice : Int<u1> -> Int<u2> -> Bytes -> Bytes
646770```
647771
648772Extract a sub-range. Arguments: start index, length, bytes.
649773
774+### `hash`
775+
776+```
777+hash : a -> Bytes
778+```
779+
780+SHA-256 hash of any value, returned as 32 bytes. `Bytes` and `Text` hash their raw contents; structured values (records, relations, constructors) hash a canonical serialisation, so equal logical values always produce equal digests.
781+
782+```knot
783+bytesToHex (hash "hello") -- "2cf24dba..."
784+```
785+
786+---
787+
788+## HTTP
789+
790+The HTTP types and primitives are defined in the language spec (`DESIGN.md`). The standard library exposes:
791+
792+### `listen` / `listenOn`
793+
794+```
795+listen : Int<u> -> Server a r -> IO {network | r} {}
796+listenOn : Text -> Int<u> -> Server a r -> IO {network | r} {}
797+```
798+
799+Start an HTTP server built with `serve API where ...`. `listen` binds to all interfaces; `listenOn` takes an explicit bind address. The `r` row variable unifies with the server's effect row, so handler effects (e.g. `console` from a handler that calls `println`) flow into the program's IO type.
800+
801+### `fetch` / `fetchWith`
802+
803+```
804+fetch : Text -> Endpoint -> IO {network} (Result HttpError T)
805+fetchWith : Text -> {headers: [{name: Text, value: Text}]}
806+ -> Endpoint -> IO {network} (Result HttpError T)
807+```
808+
809+Type-safe HTTP client built from route declarations. `Endpoint` is a route constructor; the response type `T` is inferred from the route. `fetchWith` lets you add ad-hoc headers on top of the route's declared ones. When the route declares response headers, the success body wraps as `{body: T, headers: H}` inside `Ok`.
810+
650811---
651812
652813## Cryptography
@@ -721,6 +882,19 @@ not : Bool -> Bool
721882
722883Boolean negation.
723884
885+### `stripUnit` / `withUnit` / `stripFloatUnit` / `withFloatUnit`
886+
887+```
888+stripUnit : Int<u> -> Int
889+withUnit : Int -> Int<u>
890+stripFloatUnit : Float<u> -> Float
891+withFloatUnit : Float -> Float<u>
892+```
893+
894+Drop or attach a unit tag. Identity at runtime — they only adjust the
895+compile-time type. Use them when you need to rebrand a value with a different
896+concrete unit (e.g. `Ms` → `S`).
897+
724898---
725899
726900## Built-in Traits
@@ -775,6 +949,28 @@ trait Display a where
775949
776950Convert a value to a human-readable text representation. Built-in implementations for `Int`, `Float`, `Text`, `Bool`.
777951
952+### `Sequence`
953+
954+```knot
955+trait Sequence s where
956+ take : Int -> s -> s
957+ drop : Int -> s -> s
958+```
959+
960+`take`/`drop` work on any sequenceable type. Built-in implementations for `Text` (characters) and `[]` (rows).
961+
962+### `ToJSON` / `FromJSON`
963+
964+```knot
965+trait ToJSON a where
966+ toJson : a -> Text
967+
968+trait FromJSON a where
969+ parseJson : Text -> a
970+```
971+
972+JSON encode/decode as trait methods. Built-in instances cover records, relations, primitives, ADTs, and `Maybe`/`Result`/`Bool`.
973+
778974### `Functor`
779975
780976```knot
@@ -782,7 +978,7 @@ trait Functor (f : Type -> Type) where
782978 map : (a -> b) -> f a -> f b
783979```
784980
785−Higher-kinded functor. Built-in implementation for `[]`.
981+Higher-kinded functor. Built-in implementations for `[]`, `Maybe`, `Result`, `IO`.
786982
787983### `Applicative`
788984
@@ -792,7 +988,7 @@ trait Functor f => Applicative (f : Type -> Type) where
792988 ap : f (a -> b) -> f a -> f b
793989```
794990
795−Higher-kinded applicative functor. `yield` wraps a value; `ap` applies a wrapped function. Built-in implementation for `[]`.
991+Higher-kinded applicative functor. `yield` wraps a value; `ap` applies a wrapped function. Built-in implementations for `[]`, `Maybe`, `Result`, `IO`.
796992
797993### `Monad`
798994
@@ -822,6 +1018,15 @@ trait Foldable (t : Type -> Type) where
8221018
8231019Higher-kinded foldable. Built-in implementation for `[]`.
8241020
1021+### `Traversable`
1022+
1023+```knot
1024+trait Foldable t => Traversable (t : Type -> Type) where
1025+ traverse : (a -> f b) -> t a -> f (t b)
1026+```
1027+
1028+Walk a structure left-to-right and sequence through any `Applicative` `f`. Built-in impl for `[]` over `Maybe` — useful for validating every row and collecting the result or the first `Nothing`.
1029+
8251030---
8261031
8271032## Built-in Types