appendix d

recipes

each recipe here is a complete program for one everyday job, with the output it prints. they are meant to be copied and changed. the chapters explain why each one works; a recipe names the chapter to read when you want that.

every program is a play file, so kanso play runs it as written, and every output below was recorded from a real run.

collections

count how often each item appears

list/tally answers a map from each distinct item to its count. maps keep their keys in order, so the result reads alphabetically.

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

words = text/split "the cat and the hat and the bat" " "
print "{list/tally words}"
kanso play tally.kso
{ "and":2 "bat":1 "cat":1 "hat":1 "the":3 }

group items by a key

list/group_by takes a function that computes each item's key, and answers a map from key to the items that share it, in their original order.

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

words = ["apple" "avocado" "banana" "blueberry" "cherry"]
print "{list/group_by words (w -> text/slice w 1 1)}"
kanso play group.kso
{ "a":["apple" "avocado"] "b":["banana" "blueberry"] "c":["cherry"] }

sort records by a field

list/sort_by sorts by the key a function computes, here a field read with the dot. list/map answers a lazy sequence, so list/to_list realizes it before printing (chapter 12).

sort_by.kso
import "std/list"

type order
  dish
  price

orders = [(order "ramen" 1200) (order "gyoza" 600) (order "udon" 900)]
cheapest = list/sort_by orders (o -> o.price)
print "{list/to_list (list/map cheapest (o -> o.dish))}"
kanso play sort_by.kso
["gyoza" "udon" "ramen"]

combine a list into one value

list/fold carries an accumulator through the list. for a sum or a mean, list/sum and list/mean already exist; the fold is for anything they don't cover.

fold.kso
import "std/list"

prices = [350 500 250]
total = list/fold prices 0 (sum p -> sum + p)
print "total {total}, mean {list/mean prices}"
kanso play fold.kso
total 1100, mean 366.6666666666667

use a default when a key is missing

a plain subscript answers none for a missing key, so an arm for none supplies the default. no conditional is needed (chapter 04).

or_zero.kso
fn price_of menu item
  or_zero menu[item]

fn or_zero none
  0

fn or_zero n
  n

menu = { "dango":350 "taiyaki":500 }
print "dango {price_of menu "dango"}, pocky {price_of menu "pocky"}"
kanso play or_zero.kso
dango 350, pocky 0

text

find every match of a pattern

regexp.kso
import "std/regexp"

line = "order 17 at 12:05, order 18 at 12:09"
print "{regexp/find_all "[0-9]+:[0-9]+" line}"
kanso play regexp.kso
["12:05" "12:09"]

take a file path apart

path.kso
import "std/path"

file = path/join "reports" "2026-10.csv"
print "{file}: stem {path/stem file}, extension {path/extension file}"
kanso play path.kso
reports/2026-10.csv: stem 2026-10, extension csv

files

read a file line by line

read the whole file, split it on newlines, and drop the empty line the final newline leaves. the read is an effect, so .> hands its text to the pure function that does the work (chapter 05).

menu.txt
dango 350
taiyaki 500
mochi 250
lines.kso
import "std/list"
import "std/os"
import "std/text"

fn priced line
  parts = text/split line " "
  "{parts[1]} costs {parts[2]}"

fn report body
  lines = list/reject (text/split body "\n") (l -> l == "")
  text/join (list/to_list (list/map lines priced)) "\n"

os/read_file! "menu.txt" .> report .> print
kanso play lines.kso
dango costs 350
taiyaki costs 500
mochi costs 250

decode json from a file

json/decode! insists the text is json and answers the decoded value; an object becomes a map. chapter 08 builds this decoder.

order.json
{"dish": "ramen", "price": 1200, "tags": ["hot", "noodles"]}
order.kso
import "std/json"
import "std/os"

fn described order
  "{order["dish"]} for {order["price"]}, tagged {order["tags"]}"

os/read_file! "order.json" .> json/decode! .> described .> print
kanso play order.kso
ramen for 1200, tagged ["hot" "noodles"]

when the text is not json, the failure says where the decoder stopped and rides to the endpoint like any other err. to handle it instead, use json/decode, which answers none for text that is not json.

broken.json
{"dish": ramen}
broken_json.kso
import "std/json"
import "std/os"

fn described order
  "{order["dish"]} for {order["price"]}, tagged {order["tags"]}"

os/read_file! "broken.json" .> json/decode! .> described .> print
kanso play broken_json.kso
error[endpoint]: unhandled err reached the executor: json/parse_failure 10 "unexpected character `r`"
  born in the entry at broken_json.kso:7
  passed through json/finish ← json/obj_value

the world outside the program

read the command-line arguments

os/args is an effect that answers the arguments as a list of strings. everything after -- on the command line is the program's. chapter 13 builds a full command-line tool around this.

args.kso
import "std/os"

os/args .> (a -> print "given: {a}")
kanso play args.kso -- one two
given: ["one" "two"]

read an environment variable

os/env answers none for a variable that is not set, so the default is another arm. with TEA set, the other arm answers.

env.kso
import "std/os"

fn greeting none
  "hello, stranger"

fn greeting name
  "hello, {name}"

os/env "TEA" .> greeting .> print
kanso play env.kso
hello, stranger

run another program

os/run waits for the program and answers a record with its status, stdout and stderr.

run.kso
import "std/os"
import "std/text"

fn said r
  "status {r.status}, said {text/trim r.stdout}"

os/run "echo" ["steeped"] .> said .> print
kanso play run.kso
status 0, said steeped

report an error and exit with a status

write the message to standard error, so it stays out of anything the output is piped into, then exit. the exit is built inside the bind's callback, so it runs after the write has finished.

give_up.kso
import "std/io"
import "std/os"

io/write_err "wordfreq: no such file: notes.txt\n" .> (_ -> os/exit 2)
kanso play give_up.kso
wordfreq: no such file: notes.txt

the shell sees status 2. for testing these programs, see chapter 07; for running several effects at once, chapter 06.