Skip to content

Writing kaname

A module (a .kn file) starts with its name and what it is for, then the files it reads, then its records, enums and functions in any order, and last the examples its functions must answer. A function is let lines and then its answer: there is no statement, no variable that changes and no loop, and a value is never changed once it is made. This page goes through what the four examples of a functional core are written with. The reference has the whole language.

Ranges, written or not

The types are ritsu's: a number, money and quantities in their units, a rate with its step, a date, a truth value and a string. A field or a parameter may write the range of its values, and a list or a string the count of its elements; it may also leave them out. The lines of an order write them:

record Line
  sku   : string       count >=1 <=20
  qty   : number       range >=1 <=100
  price : money[USDc]  range >=1USDc <=1_000_000USDc

A range written is what the host promises: the code a host calls checks each argument against it before it computes, and fails with input outside it. A value whose range is not written takes the whole of its type: all of int64, in its unit, for a number, an amount or a quantity, and every day from 0001-01-01 to 9999-12-31 for a date. The total of a placed order, its date and the tracking number of a shipped one have no range written. What is computed from such a value is held to int64 like anything else, and where the check cannot show it stays inside, the code checks it when it runs (What the check guarantees).

Enums with fields, and match

An enum lists its values under it, and a value may have fields of its own. The events of an order are such an enum:

enum Event
  add
    line : Line
  remove
    sku : string
  place
    on : date
  pay
    amount : money[USDc]
  ship
    tracking : string
  cancel
    reason : string

A value is made with its fields, Order.placed { lines: c.lines, total: …, on: p.on }, and taken apart by match, an arm a line, which names the value it takes to read its fields (placed p, then p.total). An arm may take more than one value, and else takes every value the arms above leave:

# whether an order takes more events
fn open(order: Order) -> bool
  match order
    shipped, cancelled => false
    else => true

A value no arm takes, an arm for a value an arm above took, and an else with nothing left are E106. A number, an amount, a date or a string is split by literals, with else. = and != compare two values of one type whole: a record field by field, a list element by element, a value of an enum by its value and its fields. { ..c, children: kept } is a record made from another, with the fields written changed.

Dicts and sets

dict[K, V] holds a value for each key, and set[T] each member once. A key is a number, an amount, a quantity, a rate with a step, a date, a string, a truth value or a value of an enum without fields, and a dict and a set go round in the order of their keys, whatever order they were made in. A builtin is written after the value it takes first:

# every code that differs, in the order of the codes: a dict and a set go round in their keys' order
fn changes(before: dict[string, number] range >=0 <=1_000_000, after: dict[string, number] range >=0 <=1_000_000) -> list[Change]
  let skus = before.keys().to_set().union(after.keys().to_set())
  skus.to_list().filter_map(sku => change(sku: sku, before: before.get(sku), after: after.get(sku)))

# the moves of a day, netted for each code
fn net(moves: list[Move] count <=1_000) -> dict[string, number]
  moves.group_by(m => m.sku).map_values(ms => ms.sum(m => m.qty))

The range of a dict is the range of its values, so every count here is 0 to 1,000,000 units. ritsu kaname run runs a function in the reference interpreter on an input of JSON. The input gives the counts as pairs, in another order than the codes' ([["ink", 4], ["pen", 10], ["pad", 2]]), and --trace shows each let:

$ cd crates/kaname
$ ritsu kaname run examples/stock_diff/stock_diff.kn changes --input tests/inputs/stock_diff.json --trace
changes(before: ["ink": 4, "pad": 2, "pen": 10], after: ["pad": 2, "pen": 7, "tape": 3])
  line 34: let skus = ["ink", "pad", "pen", "tape"].to_set()
= [Change.removed { sku: "ink", qty: 4 }, Change.changed { sku: "pen", was: 10, now: 7 }, Change.added { sku: "tape", qty: 3 }]

The code generated walks a Map of TypeScript, a dict of Python and a map of Go in the same order, so the answer is the same list in every target.

The input is written as vectors writes its in: an amount is a whole number of its unit with no unit, a rate a whole number of its steps, a date a string YYYY-MM-DD, and ritsu kaname run --help lists every type. A value of another kind fails input at the line the function is declared on, and says how its type is written. Given the counts as an object, the run answers fails: input (`changes`, declared on line 33): An argument of `changes`: `before` is dict[string, number], given as an array of its pairs `[key, value]`, in any order, each key once (`"before": [["abc", 1]]`); it is given an object.

Strings and result

A string is a sequence of code points. Its length, a cut of it and the order of two strings are in code points, in every target. White space is Unicode's, and the case a builtin changes is that of the letters of ASCII. No builtin of a string fails: parse_number answers none for a string that is not a number.

result[T, E] answers a value (ok) or what the business refuses it with (err). let x = try e takes the value of an ok, and answers an err of e as the function's own:

# the handle as it is kept: trimmed, each run of white space inside one hyphen, A to Z as a to z
fn normalized(raw: string) -> string
  raw.words().join("-").lower_ascii()
…
# a tag written `<handle>#<number>`: the handle made as `handle` makes it, the number from 1 to 9999
fn tag(text: string) -> result[Tag, list[string]]
  match text.index_of("#")
    none => err(["no # between the handle and the number"])
    some i =>
      let h = try handle(raw: text.take(i))
      match text.drop(i + 1).parse_number()
        none => err(["the number is not written in digits"])
        some n => if n >= 1 and n <= 9999 then ok(Tag { handle: h, number: n }) else err(["the number is not from 1 to 9999"])

An err is an answer, not a failure: tag is total. What kaname calls a failure is a place the check could not show, which the function's effect says and nothing in the language catches.

Generics and function values

A function, a record and an enum may take type parameters (collect[T]; K: key holds one to the types that can be keys). A parameter may be a function, given a lambda or a function by its name:

# the tree with each category's name made by the function given
fn relabel(c: Category, label: (string) -> string) -> Category
  Category { ..c, name: label(c.name), children: c.children.map(x => relabel(c: x, label: label)) }

# the names as the parts of a web address take them: words joined by hyphens, in lower case
fn slugged(c: Category) -> Category
  relabel(c: c, label: n => n.words().join("-").lower_ascii())

# what the function given picks from each category, from the top down
fn collect[T](c: Category, pick: (Category) -> list[T]) -> list[T]
  pick(c).concat(c.children.flat_map(x => collect(c: x, pick: pick)))

fn items(c: Category) -> list[Item]
  collect(c: c, pick: x => x.items)

fn names(c: Category) -> list[string]
  collect(c: c, pick: x => [x.name])

A function with type parameters or with a parameter of a function type is called by the module's functions and by no host: a host calls slugged, items and names, whose types are all known. Where such a function is called, the check reads its body again with the values and the functions the call gives. A function given carries its effect to the function that gives it, and a function type may write the effect it allows ((string) -> string total); a function given that does more is E402. No function answers a function, and no record, list or dict holds one.

Types that hold themselves

A record or an enum may hold itself, through a list, a dict, a set, an optional, a result or another type. A category of a catalog holds the categories under it:

record Category
  name     : string
  items    : list[Item]
  children : list[Category]
…
# the tree with every category that holds no item, under it or in it, taken out
fn pruned(c: Category) -> Category?
  let kept = c.children.filter_map(x => pruned(c: x))
  if c.items.count() = 0 and kept.count() = 0 then none else Category { ..c, children: kept }

A type each value of which would hold another of it, such as a record with a field of its own type that is neither optional nor in a list, is E108. A function that walks such a value recurses, so its effect is diverges: kaname does not check that it stops. A host may give a value nested 200 deep, counted one for each value of such a type along a path, and a deeper one fails with input before the function starts.

What is not in it

Input and output, the clock, random numbers and state that changes are the host's: it reads them, and gives the values to the functions it calls. There is no floating point (a value travels as a whole number inside int64, and is an exact fraction until it is rounded), no loop (reduce and recursion instead), no function that answers a function, no regular expression, and no way to catch a failure (a refusal of the business is a result). DESIGN 29 gives the reason for each, in Japanese.