builtin reference
kanso keeps its standard library small enough to hold in your head, and it draws one line through it. six names are ambient—if, 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/.
io/args
xs[k]
text/bytes
text/char_code
text/chars
text/concat
entries
text/find2
text/from_code
if
text/join
length
list/…
print
wrap_err
push
put
math/random
io/read_file
math/round
text/slice
time/sleep
math/sqrt
io/stdin
text/to_float
text/to_int
text/utf8
io/write
io/write_file
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.
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}"
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:
import "std/text"
word = text/trim " seven "
print "parsed {text/to_int word}"
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.
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]}"
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).
adapters — map 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.
consumers — fold 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.
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]}"
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:
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}"
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 }.
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}"
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.
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}"
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.
fn classify n
if (n > 0) "positive" "non-positive"
print "seven {classify 7}" >> print "zero {classify 0}"
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.
import "std/io"
fn first xs
xs[1]
pub play = io/args . first . print
<none>
<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:
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)
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 "first" >> print "second" >> print "third"
first
second
third
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 >>.
import "std/math"
import "std/time"
pub play = time/sleep 10 >> math/random 6 . show
fn show n
print "rolled {n}"
rolled 2
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.
import "std/io"
pub play =
io/write "no newline "
>> io/write "between these"
>> print "; print ends the line"
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:
import "std/io"
io/write_file "note.txt" "one taiyaki" >> io/read_file "note.txt" . print
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:
import "std/io"
pub play = io/stdin . shout . print
fn shout text
"{text}!!"
hello from a pipe!!
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
- write a program that reads a file of numbers, one per line, and prints their sum. you will need
io/read_file,text/charsor a split,text/to_int, andlist/sum—decide how a non-numeric line should behave, and make that behavior visible in the output. - using only
text/bytes,text/find2, andtext/slice, split a string on its first comma into a "before" and an "after". confirm with--planthat no i/o happens until you print the two halves. - the lenient subscript
xs[i]answers a missing index withnonewhile the strict formxs[i]!raises anerr. 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. - write a two-overload
labelfunction—one arm matchingnone, one matching a value—and drive it from amath/randomroll that indexes into a short list. run it a few times and note that the roll is the same each run.