@@ -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.