appendix b

builtin reference

kanso keeps its standard library small enough to hold in your head, and it draws one line through it. six names are ambientif, print, push, put, length, and entries resolve in any file with no import, because nearly every program touches them. everything else arrives through a std module: import "std/math" gives you math/sqrt, import "std/list" gives you list/map, and the qualified spelling in your source is the same spelling you will find in the entry headers below. the functions come in two shapes. most are ordinary pure functions—list/sort takes a list and returns a sorted one, math/sqrt takes a number and returns its root, with no trace left behind. eight are descriptions: print, io/read_file, io/write, io/write_file, io/args, io/stdin, time/sleep, and math/random don't perform i/o when evaluated—they return a value describing i/o that the runtime performs later (chapter 05). this appendix marks those description and treats the rest as plain functions.

every function here follows the same failure convention. an err or none handed in passes straight through, unlooked-at, so failures ride the railway from chapter 04 without special cases. a genuine type mismatch—list/sum on a list containing a string, math/sqrt on a record—is a program bug, not a value, and raises a runtime error at the point of the call. each entry below states both: what the function does, and what it does when the input is wrong. every example was run through the interpreter; every output block is copied from the machine, and the sources live under samples/appb/.

the sections that follow group the functions by what they operate on—numbers, text, lists, maps, bytes, choosing, and effects—and end each group with one program that exercises the lot. within a group the entries are alphabetical; the index above is the fast path when you know the name. the list section carries the largest surface, the thirty-three verbs of std/list, and gives them a reference of their own.

numbers

the numeric helpers live in std/math; the two parsers live in std/text, beside the rest of the string-to-value work.

math/round

math/round number → int

rounds a float to the nearest integer, ties away from zero (2.5 becomes 3). an int passes through unchanged, so math/round is safe to call on a value you already believe is whole. a non-number argument is a runtime error.

math/sqrt

math/sqrt number → float64

the square root, always a float—an int argument widens before the root is taken, so math/sqrt 9 is 3.0, not 3. a non-number argument is a runtime error.

text/to_float

text/to_float (string | int | float) → float64 | err

parses text into a float. an int widens and a float passes through, so the one function covers both parsing and numeric widening. text that isn't a number returns an err value carrying the offending string—a failure you can dispatch on, not a crash.

text/to_int

text/to_int (string | int) → int | err

parses text into an integer; an int passes through unchanged. unparseable text returns an err. because that err is a value, it rides the railway: interpolate it, or pipe it, and it propagates to the endpoint rather than being silently swallowed.

numbers.kso
import "std/math"
import "std/text"

root = math/sqrt 2
print "sqrt {root}"
  >> print "sqrt-int {math/sqrt 9}"
  >> print "round {math/round 2.6}"
  >> print "round-half {math/round 2.5}"
  >> print "to_int {text/to_int "42"}"
  >> print "to_float {text/to_float "3.14"}"
  >> print "to_float-int {text/to_float 5}"
kanso run numbers.kso
sqrt 1.4142135623730951
sqrt-int 3.0
round 3
round-half 3
to_int 42
to_float 3.14
to_float-int 5.0

the failure arm is worth seeing on its own. feed to_int a word and the err it returns is interpolated into the string, which propagates it—so the print never happens and the failure surfaces at main, its birthplace named:

parse_fail.kso
import "std/text"

word = text/trim " seven "

print "parsed {text/to_int word}"
kanso run parse_fail.kso
error[endpoint]: unhandled err reached the entry: ""seven" is not an integer"
  born in text/to_int at std/text/text.kso:84

text and characters

text/char_code

text/char_code string → int

the unicode code point of a one-character string—char_code "A" is 65. the argument must be exactly one character; anything longer or shorter is a runtime error. its inverse is text/from_code.

text/chars

text/chars string → string[]

splits a string into its characters, each a one-character string, one per unicode scalar. accented letters count once—chars "café" has four elements, not five. a non-string argument is a runtime error.

text/from_code

text/from_code int → string | err

the one-character string for a code point—from_code 233 is "é". an integer that is not a unicode scalar value returns an err, so surrogate halves and out-of-range numbers fail as values rather than producing garbage.

text/join

text/join string[] string → string

joins a list of strings with a separator between them—the counterpart to splitting. a failure inside the list propagates; a non-string element is a runtime error.

length

length (string | list | map) → int

a count: characters of a string, elements of a list, entries of a map. one function serves all three because "how many" means the same thing across them. anything else is a runtime error.

text/slice

text/slice (string | list) int int → (string | list)

a sub-range by 1-based inclusive positions—slice "kanso" 2 4 is "ans", and the same call shape works on lists. out-of-range or inverted bounds yield an empty result rather than a failure, so slicing never surprises you with a none to handle.

strings.kso
import "std/text"

cs = text/chars "café"
print "chars {cs}"
  >> print "length {length "café"}"
  >> print "char_code {text/char_code "A"}"
  >> print "from_code {text/from_code 233}"
  >> print "join {text/join ["a" "b" "c"] "-"}"
  >> print "slice {text/slice "kanso" 2 4}"
  >> print "concat {text/concat [1 2] [3 4]}"
kanso run strings.kso
chars ["c" "a" "f" "é"]
length 4
char_code 65
from_code é
join a-b-c
slice ans
concat [1 2 3 4]

lists

two list operations are ambient—the subscript and push—because building a list and reading one position appear in nearly every program. everything else is the std/list vocabulary, covered as a group after the two.

xs[k]

(list | string | map)[index] → value | none

indexing. a list or string is indexed by a 1-based position; a map is indexed by its key. a missing position or absent key returns none—a dispatchable value, not an exception—so the caller decides what "not there" means. the strict form xs[i]! instead raises on a miss, for the position that must be there. there is no bare function spelling; the subscript is the surface.

text/concat

text/concat (list | string) (list | string) → (list | string)

appends two lists—or two strings—into one. it lives in std/text beside slice because the pair work the same way on both shapes; the result is new and neither input is disturbed.

push

push list value → list

a new list with one value appended. it is functional: the original list is unchanged, and the value's type is unconstrained. under the hood the runtime reuses the buffer in place when the list is uniquely owned, so the value semantics cost nothing when nobody else is looking.

the std/list vocabulary

import "std/list" — 33 verbs, all subject-first

the vocabulary divides by what a verb returns. an adapter returns a lazy sequence: nothing is computed when you write list/map xs f, and each element flows through the whole chain one at a time when a consumer finally pulls. a consumer forces the sequence and returns an ordinary value. the split is why xs . list/map f . list/select p . list/sum allocates no intermediate list—and the compiler goes further, fusing the chain into a single flat scan, in this spelling or the nested one alike. every verb takes its subject first, which is what lets the pipe thread a collection through a chain without placeholders (chapter 05).

adaptersmap coll f transforms each element. select coll pred keeps the elements the predicate approves; reject coll pred drops them—the two are deliberate complements, and there is no filter. take coll n stops after n elements and drop coll n skips the first n—both are what make the infinite generators usable. zip a b pairs two sequences element by element, ending with the shorter.

generators — infinite sequences, bounded by whatever consumes them. naturals counts 1, 2, 3, …; repeat value yields the same value forever; cycle coll loops a list end over end; iterate seed stretch yields seed, stretch seed, stretch (stretch seed), …. an unbounded consumer on an unbounded generator runs forever, which in kanso is a legal program, not an error—cap with take when you want an end.

consumersfold coll init f is the root the others are built on: it threads an accumulator through every element. sum adds numbers (empty list is 0); mean averages as a float; count coll pred counts approvals; range is the statistical spread, max minus min. all? coll pred and any? coll pred answer a question about the whole—the ? suffix marks every predicate in the language. find coll pred yields the first element the predicate approves, first and last take the ends, max and min the extremes, and argmax coll key / argmin coll key the element whose key is extreme. sort orders mutually comparable elements. to_list realizes an adapter chain into a plain list—the usual last step before printing. every empty-sequence extreme (max, min, find, first, last, argmax, argmin, range) answers none, a value you dispatch on.

map builders — consumers whose result is a map. tally coll counts occurrences. group_by coll key files each element under its key, values in list buckets. index_by coll key keeps one element per key; to_h pairs builds a map from two-element pairs; transform_keys m f and transform_values m f rebuild a map through a function on each side of its entries. where two entries land on one key, the last write wins—the same rule put follows.

beneath the vocabulary sits one primitive: next, the pull protocol every adapter answers and every consumer drives. you only meet it when defining a sequence shape of your own; the other thirty-two verbs are built from it.

lists.kso
import "std/list"
import "std/text"

xs = [3 1 2]
print "map {list/to_list (list/map xs (n -> n * 10))}"
  >> print "select {list/to_list (list/select xs (n -> n > 1))}"
  >> print "sort {list/sort xs}"
  >> print "sum {list/sum xs}"
  >> print "fold {list/fold xs 10 (acc n -> acc * n)}"
  >> print "take {list/to_list (list/take list/naturals 4)}"
  >> print "zip {list/to_list (list/zip xs ["a" "b" "c"])}"
  >> print "tally {list/tally ["oat" "oat" "soy"]}"
  >> print "group_by {list/group_by [1 2 3 4] (n -> n - n / 2 * 2)}"
  >> print "range {list/range xs}"
  >> print "push {push xs 4}"
  >> print "at {xs[2]}"
  >> print "length {length xs}"
  >> print "slice {text/slice xs 1 2}"
  >> print "concat {text/concat xs [9]}"
kanso run lists.kso
map [30 10 20]
select [3 2]
sort [1 2 3]
sum 6
fold 60
take [1 2 3 4]
zip [[3 "a"] [1 "b"] [2 "c"]]
tally { "oat":2 "soy":1 }
group_by { 0:[2 4] 1:[1 3] }
range 2
push [3 1 2 4]
at 1
length 3
slice [3 1]
concat [3 1 2 9]

note xs[2] is 1—position two of [3 1 2], one-based. when the position is absent, the subscript hands back none, and because none is a value you dispatch on it. two overloads of describe—one that matches none, one that matches anything—turn "missing" into an ordinary branch:

missing.kso
fn describe none
  "index out of range"

fn describe x
  "value is {x}"

found = describe [10 20 30][2]
gone = describe [10 20 30][9]
print "in-range: {found}" >> print "missing: {gone}"
kanso run missing.kso
in-range: value is 20
missing: index out of range

the direct call describe xs[9] is deliberate. the pipe . short-circuits a failure before it reaches the function, so xs[9] . describe would propagate the none untouched; to dispatch on none you hand it to the function as an argument, where the none overload can match it.

maps

entries

entries map → entry[]

the key–value pairs of a map, each an entry key value record, returned in sorted key order. the order is deterministic, always—iteration over a map never depends on insertion history. to read a single value by key, use the subscript m[k].

put

put map key value → map

a new map with key set to value, replacing any previous binding. keys are ints or strings. like push it is functional and made in-place by the runtime when the map is uniquely owned. building a map from nothing starts at the empty-map literal {}; a literal with known keys is written { "a":1 "b":2 }.

maps.kso
blank = {}

base = { "a":1 "b":2 }
grown = put base "c" 3
print "literal {base}"
  >> print "put {grown}"
  >> print "at {grown["b"]}"
  >> print "entries {entries grown}"
  >> print "length {length grown}"
  >> print "empty {blank}"
kanso run maps.kso
literal { "a":1 "b":2 }
put { "a":1 "b":2 "c":3 }
at 2
entries [entry "a" 1 entry "b" 2 entry "c" 3]
length 3
empty {}

bytes and encoding

text/bytes

text/bytes string → int[]

the utf-8 encoding of a string as a list of byte values (0–255). an ascii string gives one byte per character; "café" gives five bytes, because é takes two. this is the entry point to byte-level work—scanning, chunking, decoding—used heavily by the json library in chapter 08.

text/find2

text/find2 int[] int int int → int

scans a byte list from a 1-based start index for the first byte equal to either of two targets—hence the 2—and returns its 1-based position. when neither target is found it returns length + 1, one past the end, a sentinel you can compare against without a branch. it is the hot primitive behind tokenizers that stop at "the next comma or brace".

text/utf8

text/utf8 int[] → string | err

the inverse of text/bytes: decodes a list of byte values back into a string. a value outside 0–255, or a byte sequence that isn't valid utf-8, returns an err—decoding untrusted bytes fails as a value you can handle, never as a crash.

bytes.kso
import "std/text"

bs = text/bytes "café"
print "bytes {bs}"
  >> print "utf8 {text/utf8 bs}"
  >> print "find2 {text/find2 (text/bytes "a,b;c") 1 44 59}"
kanso run bytes.kso
bytes [99 97 102 195 169]
utf8 café
find2 2

the find2 call searches the bytes of "a,b;c" from position 1 for a comma (44) or a semicolon (59). the comma sits at position 2, so that is the answer; had neither appeared, the result would have been 6—one past the five bytes.

choosing

if

if bool a b → value

chooses between two values by a boolean condition, and it is the only builtin that does not evaluate all its arguments: the untaken branch never runs, so if is safe to guard an expensive or failing computation. the condition must be true or false; anything else is a runtime error. in most code the dispatch of chapter 03 replaces if entirely—if is for the small, local two-way choice that isn't worth a second overload.

control.kso
fn classify n
  if (n > 0) "positive" "non-positive"

print "seven {classify 7}" >> print "zero {classify 0}"
kanso run control.kso
seven positive
zero non-positive

effects

the seven functions in this group return io values—descriptions of work, not results. evaluating them performs no i/o; they build a value the runtime executes afterward, which is why the same program can be run, planned with --plan, or fed to a scripted executor in a test (chapter 05). >> sequences two descriptions that pass no data; . pipes the value a description will yield into a function.

io/args description

io/args → io (yields string[])

yields the command-line arguments—everything after -- on the invocation—as a list of strings. the list exists only at execution time, so an index past its end is answered the way every absent index is: with none, a value the caller dispatches on. reach for xs[i]! when the position must be there and its absence should end the run.

argv.kso
import "std/io"

fn first xs
  xs[1]

pub play = io/args . first . print
kanso run argv.kso -- clay
<none>
kanso run argv.kso
<none>

wrap_err

wrap_err reason err → err

a new err carrying your reason, with the one it wraps kept as its cause. this is the only place an err is an ordinary argument instead of something that propagates: everywhere else, handing an err to anything yields that err unchanged, which is why err e cannot wrap and this function exists. the endpoint report prints the chain, each link keeping its own birthplace, so re-raising under a name of your own never loses where the trouble started:

wrap_err.kso
type config_bad
  file

type disk_torn
  path

fn low_read _
  err (disk_torn "/etc/x")

fn relabel e:err
  wrap_err (config_bad "app.conf") e

relabel (low_read 0)
kanso run wrap_err.kso
error[endpoint]: unhandled err reached the entry: config_bad "app.conf"
  born in relabel at wrap_err.kso:11
  caused by: disk_torn "/etc/x"
  born in low_read at wrap_err.kso:8

print description

print string → io

a description of writing one line to standard output. it takes a string, and only a string—to print a number or a list, interpolate it into one with "{value}". handing print a bare non-string is a runtime error with exactly that advice. sequence prints with >>; render the sequence without running it with --plan.

print.kso
print "first" >> print "second" >> print "third"
kanso run print.kso
first
second
third
kanso run print.kso --plan
first
second
third

math/random description

math/random int → io (yields int)

a description that yields an integer in the half-open range [0, bound). the scheduler's generator is deterministic, so a program's rolls are reproducible run to run—random enough for a simulation, repeatable enough for a test. pipe the yielded value into a function with ..

time/sleep description

time/sleep int → io

a description of pausing for a number of milliseconds. under the deterministic scheduler it advances virtual time rather than wall-clock time, so a program that "sleeps" is still instant to test. sequence it with >>.

timing.kso
import "std/math"
import "std/time"

pub play = time/sleep 10 >> math/random 6 . show

fn show n
  print "rolled {n}"
kanso run timing.kso
rolled 2
kanso run timing.kso --plan
rolled 2

the plan names sleep and random up front, then stops at . <continuation>—honest notation for "what show does depends on a value the roll hasn't produced yet." that is the visible seam between >>, which lists every step, and ., which threads a not-yet-known value forward.

io/read_file description

io/read_file string → io (yields string)

a description that yields the contents of a file as a string. a missing or unreadable path fails at execution time—at the executor, when the read is actually attempted—rather than when the description is built. pipe the contents into a function with ..

io/write description

io/write string → io

a description of writing a string to standard output exactly as given—no newline. print is the whole-line form; write is for assembling a line from pieces, and for streaming: a chain that writes a chunk and binds the rest (io/write chunk . (_ -> more)) builds each chunk only when the executor reaches it, so output of any size flows in constant memory.

write_stream.kso
import "std/io"

pub play =
  io/write "no newline "
  >> io/write "between these"
  >> print "; print ends the line"
kanso run write_stream.kso
no newline between these; print ends the line

io/write_file description

io/write_file string string → io

a description of writing a string to a path, replacing any existing contents. it yields nothing useful, so it is normally sequenced with >> before the next step. here one program writes a file and then reads it straight back:

files.kso
import "std/io"

io/write_file "note.txt" "one taiyaki" >> io/read_file "note.txt" . print
kanso run files.kso
one taiyaki

io/stdin description

io/stdin → io (yields string)

a description that yields all of standard input as a single string. pipe it into a function to process what was fed on the pipe:

input.kso
import "std/io"

pub play = io/stdin . shout . print

fn shout text
  "{text}!!"
printf 'hello from a pipe' | kanso run input.kso
hello from a pipe!!
err - always arrives, never uninvited
err

hand me to a builtin and i pass straight through—list/sort, list/map, list/sum, none of them inspect me, they just return me. a type mistake is different: list/sum on a list with a string in it is a bug in your program, and that raises where it happens. failures flow; bugs stop.

that is the whole surface: six ambient names, a handful of std modules, two shapes, one failure convention. (std/json, the decoder the book builds by hand in chapter 08, ships ready-made too.) you now have the signature, the behavior, and the failure mode of everything kanso offers—enough to read any program in this book without guessing what a call does, and enough to reach for the right verb instead of hand-rolling it. the chapters build real programs on exactly this surface; the json library of chapter 08 is little more than text/bytes, text/find2, and text/slice worn smooth.

exercises

  1. write a program that reads a file of numbers, one per line, and prints their sum. you will need io/read_file, text/chars or a split, text/to_int, and list/sum—decide how a non-numeric line should behave, and make that behavior visible in the output.
  2. using only text/bytes, text/find2, and text/slice, split a string on its first comma into a "before" and an "after". confirm with --plan that no i/o happens until you print the two halves.
  3. the lenient subscript xs[i] answers a missing index with none while the strict form xs[i]! raises an err. write one small program that shows both on the same out-of-range access, and explain in a comment which you would choose for a lookup that is expected to miss.
  4. write a two-overload label function—one arm matching none, one matching a value—and drive it from a math/random roll that indexes into a short list. run it a few times and note that the roll is the same each run.