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.
import "std/list"
import "std/text"
words = text/split "the cat and the hat and the bat" " "
print "{list/tally words}"
{ "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.
import "std/list"
import "std/text"
words = ["apple" "avocado" "banana" "blueberry" "cherry"]
print "{list/group_by words (w -> text/slice w 1 1)}"
{ "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).
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))}"
["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.
import "std/list"
prices = [350 500 250]
total = list/fold prices 0 (sum p -> sum + p)
print "total {total}, mean {list/mean prices}"
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).
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"}"
dango 350, pocky 0
text
find every match of a pattern
import "std/regexp"
line = "order 17 at 12:05, order 18 at 12:09"
print "{regexp/find_all "[0-9]+:[0-9]+" line}"
["12:05" "12:09"]
take a file path apart
import "std/path"
file = path/join "reports" "2026-10.csv"
print "{file}: stem {path/stem file}, extension {path/extension file}"
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).
dango 350
taiyaki 500
mochi 250
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
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.
{"dish": "ramen", "price": 1200, "tags": ["hot", "noodles"]}
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
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.
{"dish": ramen}
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
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.
import "std/os"
os/args .> (a -> print "given: {a}")
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.
import "std/os"
fn greeting none
"hello, stranger"
fn greeting name
"hello, {name}"
os/env "TEA" .> greeting .> print
hello, stranger
run another program
os/run waits for the program and answers a record with its status, stdout and stderr.
import "std/os"
import "std/text"
fn said r
"status {r.status}, said {text/trim r.stdout}"
os/run "echo" ["steeped"] .> said .> print
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.
import "std/io"
import "std/os"
io/write_err "wordfreq: no such file: notes.txt\n" .> (_ -> os/exit 2)
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.