Skip to content

Four functional cores

The first four examples of kaname read no other language. Each is in crates/kaname/examples/, in English and, beside it, in Japanese (<name>.ja.kn), and each has a host under host/: a program in TypeScript, one in Python and one in Go, which calls the code ritsu gen writes of the module and prints what host/output.txt holds. ritsu's tests build each package and run the twelve programs against it, the TypeScript through tsc --strict, the Python through mypy --strict and the Go through go vet.

The package a host calls

In the directory of an example, ritsu gen writes a package for each of TypeScript, Python and Go:

$ cd crates/kaname/examples/order_flow
$ ritsu gen --root . --out gen --module example.com/order_flow
generated: gen/typescript/glue/kaname.ts
generated: gen/typescript/glue/order_flow_ja.ts
generated: gen/typescript/glue/order_flow.ts
…
generated: gen/python/generated/glue/order_flow.py
…
generated: gen/go/glue/orderflow/orderflow.go
…

A module is under glue/, beside the helper module kaname, which holds the exact fractions, the arithmetic checked against int64, the roundings, the strings counted in code points and the dicts and sets walked in the order of their keys. The package uses nothing but each language's standard library. A host is put beside it: the TypeScript in gen/typescript/, the Python in gen/python/, where the package is generated, and the Go in a directory of its own in the Go module --module names. The values travel in each language's own types:

kaname TypeScript Python Go
number bigint int int64
money[USDc] the package's USDc USDc units.USDc
date "2026-10-22" datetime.date kaname.Date
a record a read-only object a frozen data class a struct
a value of an enum with fields an object with its kind a data class (EventAdd) a struct (EventAdd)
result[T, E] { kind: "ok", value }, { kind: "err", error } kaname.Ok, kaname.Err kaname.Ok[T, E], kaname.Err[T, E]
dict[K, V], set[T] ReadonlyMap, ReadonlySet a dict, a frozenset map[K]V, map[T]struct{}

The function a host calls checks its arguments against what they declare before it computes, and fails with input outside them. Every failure is a KanameError with its kind: input, or one of the kinds the function's effect says. A refusal of the business is not a failure but a result, which the host reads.

An order and its events

order_flow moves an order through five states (the module is on the first page). The host keeps the order and hands next each event as it comes, keeping the order next answers, or printing why the event is refused:

# the order an event moves it to, or why the event is refused
fn next(order: Order, event: Event) -> result[Order, string]
…
# the events in turn, from an empty cart: the first refusal stops them
fn replay(events: list[Event]) -> result[Order, string]
  let start : result[Order, string] = ok(Order.cart { lines: [] })
  events.reduce(start, (r, e) => after(r: r, event: e))
import { next, type Event, type Order } from "./glue/order_flow";
import type { USDc } from "./units";

const events: Event[] = [
  { kind: "add", line: { sku: "pen", qty: 2n, price: 150n as USDc } },
  { kind: "add", line: { sku: "ink", qty: 1n, price: 400n as USDc } },
  { kind: "place", on: "2026-10-22" },
  { kind: "pay", amount: 650n as USDc },
  { kind: "pay", amount: 700n as USDc },
  { kind: "ship", tracking: "1Z-42" },
  { kind: "cancel", reason: "too late" },
];

let order: Order = { kind: "cart", lines: [] };
for (const event of events) {
  const r = next(order, event);
  if (r.kind === "err") {
    console.log(`${event.kind}: refused: ${r.error}`);
    continue;
  }
  order = r.value;
  console.log(`${event.kind}: ${order.kind}`);
}
import datetime

from generated.glue import kaname, order_flow as flow
from generated.units import USDc

events: list[flow.Event] = [
    flow.EventAdd(line=flow.Line(sku="pen", qty=2, price=USDc(150))),
    flow.EventAdd(line=flow.Line(sku="ink", qty=1, price=USDc(400))),
    flow.EventPlace(on=datetime.date(2026, 10, 22)),
    flow.EventPay(amount=USDc(650)),
    flow.EventPay(amount=USDc(700)),
    flow.EventShip(tracking="1Z-42"),
    flow.EventCancel(reason="too late"),
]
# the name of each value, as the module writes it
names = {
    flow.EventAdd: "add", flow.EventPlace: "place", flow.EventPay: "pay", flow.EventShip: "ship", flow.EventCancel: "cancel",
    flow.OrderCart: "cart", flow.OrderPlaced: "placed", flow.OrderPaid: "paid", flow.OrderShipped: "shipped", flow.OrderCancelled: "cancelled",
}

order: flow.Order = flow.OrderCart(lines=[])
for event in events:
    r = flow.next(order=order, event=event)
    if isinstance(r, kaname.Err):
        print(f"{names[type(event)]}: refused: {r.error}")
        continue
    order = r.value
    print(f"{names[type(event)]}: {names[type(order)]}")
package main

import (
    "fmt"

    "example.com/order_flow/glue/kaname"
    flow "example.com/order_flow/glue/orderflow"
    "example.com/order_flow/units"
)

…

func main() {
    events := []flow.Event{
        flow.EventAdd{Line: flow.Line{Sku: "pen", Qty: 2, Price: units.USDc(150)}},
        flow.EventAdd{Line: flow.Line{Sku: "ink", Qty: 1, Price: units.USDc(400)}},
        flow.EventPlace{On: kaname.Date{Year: 2026, Month: 10, Day: 22}},
        flow.EventPay{Amount: units.USDc(650)},
        flow.EventPay{Amount: units.USDc(700)},
        flow.EventShip{Tracking: "1Z-42"},
        flow.EventCancel{Reason: "too late"},
    }
    var order flow.Order = flow.OrderCart{}
    for _, event := range events {
        switch r := flow.Next(order, event).(type) {
        case kaname.Err[flow.Order, string]:
            fmt.Printf("%s: refused: %s\n", name(event), r.Error)
        case kaname.Ok[flow.Order, string]:
            order = r.Value
            fmt.Printf("%s: %s\n", name(event), name(order))
        }
    }
}
add: cart
add: cart
place: placed
pay: refused: the amount is not the total
pay: paid
ship: shipped
cancel: refused: the order is closed

A value of an enum with fields is split on its kind in TypeScript, with isinstance in Python and with a type switch in Go, and a result the same way. replay runs a list of events through next in the module itself, and stops at the first refusal, where the host goes on to the next event.

Two counts of a stockroom

stock_diff keeps a count as a dict from a code to how many are on the shelf, and compares two counts code by code. It also lays the moves of a day on a count, and answers the codes to reorder as a set:

record Move
  sku : string  count >=1 <=20
  qty : number  range >=-10_000 <=10_000

enum Change
  added
    sku : string
    qty : number
  removed
    sku : string
    qty : number
  changed
    sku  : string
    was : number
    now : number
…
fn changes(before: dict[string, number] range >=0 <=1_000_000, after: dict[string, number] range >=0 <=1_000_000) -> list[Change]
…
# a count with the moves of a day laid on it; a code that comes to none is left out
fn moved(stock: dict[string, number] count <=10_000 range >=0 <=1_000_000, moves: list[Move] count <=1_000) -> dict[string, number]
  stock.merge_with(net(moves: moves), (a, b) => a + b).filter(e => e.value != 0)

# the codes on the shelf below what is kept of them
fn to_reorder(stock: dict[string, number] range >=0 <=1_000_000, floor: number range >=0 <=1_000_000) -> set[string]
  stock.filter(e => e.value < floor).keys().to_set()
import { changes, moved, to_reorder } from "./glue/stock_diff";

const before = new Map([["ink", 4n], ["pen", 10n], ["pad", 2n]]);
const after = new Map([["pen", 7n], ["pad", 2n], ["tape", 3n]]);
for (const c of changes(before, after)) {
  switch (c.kind) {
    case "added":
      console.log(`added ${c.sku}: ${c.qty}`);
      break;
    case "removed":
      console.log(`removed ${c.sku}: ${c.qty}`);
      break;
    case "changed":
      console.log(`changed ${c.sku}: ${c.was} -> ${c.now}`);
      break;
  }
}
const today = moved(after, [{ sku: "pen", qty: -7n }, { sku: "glue", qty: 12n }]);
console.log(`after the day: ${[...today].map(([sku, n]) => `${sku} ${n}`).join(", ")}`);
console.log(`to reorder: ${[...to_reorder(today, 5n)].join(", ")}`);
from generated.glue import stock_diff as stock

before = {"ink": 4, "pen": 10, "pad": 2}
after = {"pen": 7, "pad": 2, "tape": 3}
for c in stock.changes(before=before, after=after):
    if isinstance(c, stock.ChangeAdded):
        print(f"added {c.sku}: {c.qty}")
    elif isinstance(c, stock.ChangeRemoved):
        print(f"removed {c.sku}: {c.qty}")
    else:
        print(f"changed {c.sku}: {c.was} -> {c.now}")
today = stock.moved(stock=after, moves=[stock.Move(sku="pen", qty=-7), stock.Move(sku="glue", qty=12)])
print("after the day: " + ", ".join(f"{sku} {n}" for sku, n in today.items()))
print("to reorder: " + ", ".join(sorted(stock.to_reorder(stock=today, floor=5))))
package main

import (
    "fmt"
    "maps"
    "slices"
    "strings"

    stock "example.com/stock_diff/glue/stockdiff"
)

func main() {
    before := map[string]int64{"ink": 4, "pen": 10, "pad": 2}
    after := map[string]int64{"pen": 7, "pad": 2, "tape": 3}
    for _, c := range stock.Changes(before, after) {
        switch c := c.(type) {
        case stock.ChangeAdded:
            fmt.Printf("added %s: %d\n", c.Sku, c.Qty)
        case stock.ChangeRemoved:
            fmt.Printf("removed %s: %d\n", c.Sku, c.Qty)
        case stock.ChangeChanged:
            fmt.Printf("changed %s: %d -> %d\n", c.Sku, c.Was, c.Now)
        }
    }
    today := stock.Moved(after, []stock.Move{{Sku: "pen", Qty: -7}, {Sku: "glue", Qty: 12}})
    var counts []string
    for _, sku := range slices.Sorted(maps.Keys(today)) {
        counts = append(counts, fmt.Sprintf("%s %d", sku, today[sku]))
    }
    fmt.Println("after the day: " + strings.Join(counts, ", "))
    fmt.Println("to reorder: " + strings.Join(slices.Sorted(maps.Keys(stock.ToReorder(today, 5))), ", "))
}
removed ink: 4
changed pen: 10 -> 7
added tape: 3
after the day: glue 12, pad 2, tape 3
to reorder: pad, tape

The changes come in the order of the codes in all three, since a dict goes round in the order of its keys. A Python frozenset and a Go map have no order of their own, so the Python host sorts the codes to reorder, and the Go host sorts both what it prints.

What a person typed

handles makes a handle a person typed the one a service keeps, then holds it to what a handle is: 3 to 20 characters, a to z, 0 to 9 and -, starting with a letter, and not a word the service keeps. It answers every reason a handle is refused, and reads a tag, a handle and a number:

record Tag
  handle : string
  number : number  range >=1 <=9999
…
# each reason a handle is not one the service takes, in order
fn problems(handle: string) -> list[string]
  let n = handle.count()
  let short = if n < 3 then "shorter than 3 characters" else none
  let long = if n > 20 then "longer than 20 characters" else none
  let odd = if handle.chars().all(c => "abcdefghijklmnopqrstuvwxyz0123456789-".contains(c)) then none else "a character other than a to z, 0 to 9 and -"
  let start = if "abcdefghijklmnopqrstuvwxyz".contains(handle.take(1)) then none else "not starting with a letter"
  let taken = if ["admin", "help", "root", "support"].contains(handle) then "a word the service keeps" else none
  [short, long, odd, start, taken].filter_map(x => x)

# the handle a person typed, as it is kept, or every reason it cannot be
fn handle(raw: string) -> result[string, list[string]]
  let h = normalized(raw: raw)
  let why = problems(handle: h)
  if why.count() = 0 then ok(h) else err(why)
…
fn tag(text: string) -> result[Tag, list[string]]
import { handle, tag } from "./glue/handles";

for (const typed of [" Grace Hopper", "9", "Root", "ünïcode"]) {
  const r = handle(typed);
  console.log(r.kind === "ok" ? `"${typed}" -> ${r.value}` : `"${typed}" -> refused: ${r.error.join("; ")}`);
}
for (const typed of ["Ada Lovelace#42", "ada#0"]) {
  const r = tag(typed);
  console.log(r.kind === "ok" ? `"${typed}" -> ${r.value.handle} #${r.value.number}` : `"${typed}" -> refused: ${r.error.join("; ")}`);
}
from generated.glue import handles, kaname

for typed in [" Grace Hopper", "9", "Root", "ünïcode"]:
    r = handles.handle(raw=typed)
    print(f'"{typed}" -> {r.value}' if isinstance(r, kaname.Ok) else f'"{typed}" -> refused: {"; ".join(r.error)}')
for typed in ["Ada Lovelace#42", "ada#0"]:
    t = handles.tag(text=typed)
    print(f'"{typed}" -> {t.value.handle} #{t.value.number}' if isinstance(t, kaname.Ok) else f'"{typed}" -> refused: {"; ".join(t.error)}')
package main

import (
    "fmt"
    "strings"

    "example.com/handles/glue/handles"
    "example.com/handles/glue/kaname"
)

func main() {
    for _, typed := range []string{" Grace Hopper", "9", "Root", "ünïcode"} {
        switch r := handles.Handle(typed).(type) {
        case kaname.Ok[string, []string]:
            fmt.Printf("%q -> %s\n", typed, r.Value)
        case kaname.Err[string, []string]:
            fmt.Printf("%q -> refused: %s\n", typed, strings.Join(r.Error, "; "))
        }
    }
    for _, typed := range []string{"Ada Lovelace#42", "ada#0"} {
        switch r := handles.TagFn(typed).(type) {
        case kaname.Ok[handles.Tag, []string]:
            fmt.Printf("%q -> %s #%d\n", typed, r.Value.Handle, r.Value.Number)
        case kaname.Err[handles.Tag, []string]:
            fmt.Printf("%q -> refused: %s\n", typed, strings.Join(r.Error, "; "))
        }
    }
}
" Grace Hopper" -> grace-hopper
"9" -> refused: shorter than 3 characters; not starting with a letter
"Root" -> refused: a word the service keeps
"ünïcode" -> refused: a character other than a to z, 0 to 9 and -; not starting with a letter
"Ada Lovelace#42" -> ada-lovelace #42
"ada#0" -> refused: the number is not from 1 to 9999

The generated code counts, cuts and compares a string in code points in all three targets, though a TypeScript string is made of units of UTF-16 and a Go string of bytes: "ünïcode" is 7 characters, and 9 bytes in Go. ü and ï are not among the characters a handle takes, and ü is not one of the letters a to z it has to start with. In Go, tag is TagFn, as the module has a record Tag too.

A tree of categories

catalog keeps a catalog as a tree: a category holds its items and the categories under it. The module answers the path of every item, the tree with its empty categories taken out, the tree with its names made fit for a web address, every name, and the cheapest item:

record Item
  sku   : string
  name  : string
  price : money[USDc]  range >=1USDc <=1_000_000USDc

record Category
  name     : string
  items    : list[Item]
  children : list[Category]

# every item with the names of the categories it is under: `Garden / Tools / Spade`
fn paths(c: Category) -> list[string]
  paths_under(c: c, above: [])

fn paths_under(c: Category, above: list[string]) -> list[string]
  let here = above.append(c.name)
  c.items.map(i => here.append(i.name).join(" / ")).concat(c.children.flat_map(x => paths_under(c: x, above: here)))
…
# the cheapest item of the tree, the first of those of one price
fn cheapest(c: Category) -> Item?
  items(c: c).min_by(i => i.price)
import { cheapest, names, paths, pruned, slugged, type Category } from "./glue/catalog";
import type { USDc } from "./units";

const garden: Category = {
  name: "Garden",
  items: [],
  children: [
    { name: "Hand Tools", items: [{ sku: "sp-1", name: "Spade", price: 1999n as USDc }, { sku: "tr-2", name: "Trowel", price: 899n as USDc }], children: [] },
    { name: "Seeds", items: [], children: [{ name: "Bulbs", items: [], children: [] }] },
    { name: "Hoses", items: [{ sku: "hr-1", name: "Hose reel", price: 4500n as USDc }], children: [] },
  ],
};
for (const p of paths(garden)) {
  console.log(p);
}
const kept = pruned(garden);
console.log(`kept: ${kept === null ? "nothing" : names(kept).join(", ")}`);
console.log(`slugs: ${names(slugged(garden)).join(", ")}`);
const c = cheapest(garden);
console.log(`cheapest: ${c === null ? "none" : `${c.name}, ${c.price} cents`}`);
from generated.glue import catalog
from generated.units import USDc

garden = catalog.Category(
    name="Garden",
    items=[],
    children=[
        catalog.Category(
            name="Hand Tools",
            items=[catalog.Item(sku="sp-1", name="Spade", price=USDc(1999)), catalog.Item(sku="tr-2", name="Trowel", price=USDc(899))],
            children=[],
        ),
        catalog.Category(name="Seeds", items=[], children=[catalog.Category(name="Bulbs", items=[], children=[])]),
        catalog.Category(name="Hoses", items=[catalog.Item(sku="hr-1", name="Hose reel", price=USDc(4500))], children=[]),
    ],
)
for p in catalog.paths(c=garden):
    print(p)
kept = catalog.pruned(c=garden)
print("kept: " + ("nothing" if kept is None else ", ".join(catalog.names(c=kept))))
print("slugs: " + ", ".join(catalog.names(c=catalog.slugged(c=garden))))
c = catalog.cheapest(c=garden)
print("cheapest: " + ("none" if c is None else f"{c.name}, {c.price} cents"))
package main

import (
    "fmt"
    "strings"

    "example.com/catalog/glue/catalog"
    "example.com/catalog/units"
)

func main() {
    garden := catalog.Category{
        Name: "Garden",
        Children: []catalog.Category{
            {Name: "Hand Tools", Items: []catalog.Item{{Sku: "sp-1", Name: "Spade", Price: units.USDc(1999)}, {Sku: "tr-2", Name: "Trowel", Price: units.USDc(899)}}},
            {Name: "Seeds", Children: []catalog.Category{{Name: "Bulbs"}}},
            {Name: "Hoses", Items: []catalog.Item{{Sku: "hr-1", Name: "Hose reel", Price: units.USDc(4500)}}},
        },
    }
    for _, p := range catalog.Paths(garden) {
        fmt.Println(p)
    }
    if kept := catalog.Pruned(garden); kept == nil {
        fmt.Println("kept: nothing")
    } else {
        fmt.Println("kept: " + strings.Join(catalog.Names(*kept), ", "))
    }
    fmt.Println("slugs: " + strings.Join(catalog.Names(catalog.Slugged(garden)), ", "))
    if c := catalog.Cheapest(garden); c == nil {
        fmt.Println("cheapest: none")
    } else {
        fmt.Printf("cheapest: %s, %d cents\n", c.Name, c.Price)
    }
}
Garden / Hand Tools / Spade
Garden / Hand Tools / Trowel
Garden / Hoses / Hose reel
kept: Garden, Hand Tools, Hoses
slugs: garden, hand-tools, seeds, bulbs, hoses
cheapest: Trowel, 899 cents

The functions walk the tree, so they recurse, and their effect is diverges: kaname does not check that a walk stops, and the check says so (9 functions, diverges). relabel and collect, which take functions, are no host's to call; the host calls slugged, names and cheapest, which call them (Writing kaname). An optional is null in TypeScript, None in Python and a nil pointer in Go.