chapter 03

types and dispatch

most languages hand you a whole drawer of tools for the question "what kind of thing is this?"—switch, match, instanceof, a type tag you test by hand, a chain of if you read top to bottom. kanso gives you exactly one, and it doubles as the way you build data and take it back apart. once it is familiar, most of the conditionals you would write in another language become definitions instead. we start with how data goes together.

records

every type in kanso is a single-constructor record: a name and some fields. the fields come in the order you declare them. a field is just a name; its type is whatever the program puts in it and asks of it, which the compiler works out for itself. construction is positional, using the type name as the constructor and passing fields in that same declared order:

writing the type on a field is a compile error. artist:string gets you a record field carries no type—write artist and let the compiler infer what it holds, because the annotation only repeats what the program already says and then sits there waiting to disagree with it. the language server shows you what a field actually holds when you hover it, which is the same information without a second copy to maintain. the one kind of type you do write down is a typeset, later in this chapter.

reading.kso
type track
  artist
  minutes
  title

song = track "fishmans" 6 "long season"
track artist minutes title = song
print "{title}, by {artist}—{minutes} minutes"
  .> (_ -> print "or just the one field: {song.title}")
output
long season, by fishmans—6 minutes
or just the one field: long season

the third line is the other half of the story. you read a record by writing its constructor shape on the left of = and letting every field bind to a name. that is a binding pattern: the same shape you built with, running in reverse.

when one field is all you want, a dot reads it: song.title. the two forms answer different questions. a binding pattern says you are about to use the whole record and want its parts named; a dot says you want one value and are done. reach for the pattern when you would otherwise write three dots in a row.

leave the record out and you have the reader on its own: _.title is a function that takes a track and gives you its title. it goes wherever a function goes.

list/map songs _.title

this works for any record with a title, because a field reader is an ordinary arm and every type that has the field contributes one. nothing has to be imported to read a field — the reader is not a name in your file, so it can never collide with one.

reading is where the dot stops. there is no song.title = "…", because a record is a value and a value does not change under you. if you want a track with a different title, you build a track with a different title.

taking a record apart

the positional form binds every field, which is honest but often more than you want. when you only care about a couple of fields, use the keyed read—name the fields you want inside braces, rename freely with :, and leave the rest unmentioned:

keyed.kso
type track
  artist
  minutes
  title

song = track "fishmans" 6 "long season"
{ artist:who title } = song
print "{title}, by {who}"
output
long season, by fishmans

the keyed form is by name, so it needs no field order and no placeholder for the fields it skips. its one rule: a keyed read must omit at least one field—if you wanted all of them you would have used the positional form. if you instead try to split the difference, positional destructuring with _ for the field you would rather not name, the compiler stops you and points at the tool that already does the job:

peek_check.kso
type track
  artist
  minutes
  title

pub play =
  song = track "fishmans" 6 "long season"
  track artist _ title = song
  print "{title}, by {artist}"
kanso check peek_check.kso
error[syntax]: `_` does not appear in binding patterns; omit fields with a keyed read
  --> peek_check.kso:8:16
   8 |   track artist _ title = song
                      ^

the third place a record comes apart is a function head, and there _ is welcome—a parameter pattern selects the type and unpacks it in one gesture, discarding what it doesn't use:

credit.kso
type track
  artist
  minutes
  title

fn credit (track artist _ title)
  "{title}—{artist}"

song = track "fishmans" 6 "long season"
print (credit song)
output
long season—fishmans

that parameter pattern is also how kanso chooses between overloads, which is the subject of the rest of the chapter.

two records that point at each other

ada's partner is bob and bob's partner is ada. neither can be built first, because each one needs the other to already exist. most languages answer this by making the field nullable and checking it forever after, or by building both halves empty and patching the links in a second pass.

kanso ties the knot with list/tie, from the standard library. you hand it a list of keys and a maker, a function of two arguments. tie calls the maker once for each key, passing the key and a function called ref; ref k stands for the node the maker builds for k. every record comes out whole, each field holding its final value, and tie answers a map from each key to its node:

knot.kso
import "std/list"

type person
  name
  partner

fn met "ada" ref
  person "ada" (ref "bob")

fn met name ref
  person name (ref "ada")

people = list/tie ["ada" "bob"] met
ada = people["ada"]

print "{ada.name} and {ada.partner.name}"
  .> (_ -> print "back where we started: {ada.partner.partner.name}")
output
ada and bob
back where we started: ada

the maker is an ordinary overloaded function: met builds ada with a link to bob, and everyone else with a link to ada. ref "bob" stands for bob before bob has been made, and tie fills the link in once every call has returned, so no code ever sees a record with a field still to come. the type can say partner:person instead of some nullable stand-in.

a field can hold several links at once. town name [(ref "birch") (ref "cedar")] stores two references in one list field, and a map literal of references works the same way. the maker may store such a list but not read it: indexing it, measuring it or passing it to another function while the nodes are being made is refused, because each of those would look through a reference that is not filled in yet.

a link can name a key that is not in the list. when the keys and the links are literals, as they are here, the compiler sees the missing key and refuses the program with error[tie]. when the keys are computed, the answer is only known at run time, and tie answers a list/broken_link naming the key in place of the map; a program that builds its keys from data takes that apart with an overload on b:list/broken_link before it reads a node.

what comes back is ordinary immutable data that happens to contain a cycle. you can walk the ring as far as you like—ada.partner.partner is ada again—and nothing in it can be assigned. chapter 15 returns to the memory side: everything one call to tie creates is born together and released together, so a cycle costs the runtime no more than a straight line.

the only switch

kanso has no match, no instanceof, no tag test, no narrowing syntax. a value may be one of several types—an int or a string, a result or an err—and the only way to take that apart is to define overloads and let dispatch choose. the conditional you would have written lives in the function head instead. write the same name several times, each with a different pattern, and each call goes to the arm that fits its argument:

describe.kso
fn describe 0
  "zero"

fn describe n:int
  "the int {n}"

fn describe s:string
  "the string {s}"

fn describe x
  "something else: {x}"

print (describe 0)
  .> (_ -> print (describe 7))
  .> (_ -> print (describe "seven"))
  .> (_ -> print (describe 1.5))
output
zero
the int 7
the string seven
something else: 1.5

four arms, one name, and a specificity ladder deciding who answers. a literal like 0 is the most specific thing you can ask for, so it wins over the ascribed type n:int, which in turn wins over the bare generic x. the ladder, top to bottom: literal, then single concrete type (a wrapper before the type it wraps), then typeset, which comes later in this chapter, then the unannotated generic that catches everything left. note the ascription is tight: n:int, no spaces. n: int is a formatting error. when an arm needs the type and not the value, ascribe the wildcard: _:int dispatches on membership and binds nothing.

dispatch on a literal is what turns the classic recursive definition into something you can read aloud. the base case is an overload, not an if guarding the real body:

fact.kso
fn fact 0
  1

fn fact n
  n * fact (n - 1)

answer = fact 20
print "20! = {answer}"
output
20! = 2432902008176640000

that answer doesn't fit in 64 bits and nobody had to care—int is arbitrary-precision, as chapter 02 promised.

markers: a type with no fields

a record's fields are optional. a type with no field lines at all is a marker: the bare mention of its name is its only value. true, false, and none are markers the standard library declares this way—none of them is a keyword. you declare your own the same way, and dispatch reads them like any other type:

marker.kso
type null

fn describe null
  "nothing here"

fn describe x
  "got {x}"

print (describe null) .> (_ -> print (describe 5))
output
nothing here
got 5

the same word null is the type, the value, and the pattern—there is nothing to disambiguate, because a binding can never be named after a declared type (kanso forbids shadowing). a marker holds nothing, so passing it fields is meaningless, and the compiler says so:

markerarg_check.kso
type null

pub play =
  x = null 5
  print "{x}"
kanso check markerarg_check.kso
error[signature]: `null` takes no fields; its bare mention is its value
  --> markerarg_check.kso:4:7
   4 |   x = null 5
             ^

a handful of markers with one dispatch group over them is an enumeration. kanso has no enum keyword because it needs none:

status.kso
type active

type archived

type pending

fn label active
  "live"

fn label archived
  "in the vault"

fn label pending
  "coming soon"

print (label active)
  .> (_ -> print (label archived))
  .> (_ -> print (label pending))
output
live
in the vault
coming soon

typesets: naming a set of types

a field holds whatever the program puts in it, and the compiler works that out. sometimes you want to say something the program does not otherwise say — that a post is archived, a draft, or published, and never a fourth thing. give the set a name:

membership.kso
type archived

type draft

type published

type state archived draft published

type post
  state
  title

fn label archived
  "archived"

fn label draft
  "a draft"

fn label published
  "live"

p = post draft "long season"
print "{p.title} is {label p.state}"
kanso play membership.kso
long season is a draft

type state archived draft published is a typeset: one line, members alphabetical, no fields of its own. it is a name for "one of these", and once it exists you can dispatch on it, annotate a parameter with it, and read it in an error message. the field itself stays bare — state — because what that field holds is still something the compiler derives from the program.

this is the one place a written type earns its keep. everywhere else the compiler already knows and writing it down only adds a second copy to disagree with the first; here you are saying something it could not have worked out, because a set nobody has yet exceeded looks exactly like a set nobody can exceed.

wrappers: one type standing in for another

a typeset names a set of types you already have. the other direction names a new type that is one you already have: cents are integers, a post body is a string. one line declares it, child first and parent after, and the compiler can then tell them apart where you care and treat them alike where you don't:

wrappers.kso
type cents int

type post_body string

fn to_string _:cents
  "a price"

fn quote c:cents
  "{c} for the set"

fn quote n:int
  "{n} plain"

print (quote (cents 350))
  .> (_ -> print (quote 350))
  .> (_ -> print "a body reads through: {post_body "hello"}")
kanso play wrappers.kso
a price for the set
350 plain
a body reads through: hello

cents 350 builds one, and the wrapper sits one rung above its parent on the ladder: quote (cents 350) takes the c:cents arm while a bare 350 falls through to n:int. arms still appear most-specific first, so the wrapper's arm is written above the parent's. a chain may run deeper than one link, and the nearer declaration always answers.

a wrapper flows wherever its parent flows, and that includes printing: post_body "hello" renders as hello, because a string is what it is. adding a to_string arm for the type changes that, which is what makes the price above read a price. appendix a explains the rule that permits it — an arm may claim a type its own module declares, which is exactly what a wrapper is.

the ladder is law

because dispatch stands in for every switch you would otherwise write, kanso is strict about how an overload group is laid out. the arms appear most-specific first, so the file reads in the order the dispatcher resolves. write them backwards and it is a formatting error, with the fix stated as the rule:

backwards_check.kso
fn fact n
  n * fact (n - 1)

fn fact 0
  1
kanso check backwards_check.kso
error[formatting]: overloads of `fact` appear most-specific first: literal, then concrete type, then generic
  --> backwards_check.kso:4:4
   4 | fn fact 0
          ^

and two arms that dispatch could never tell apart are rejected outright—a switch with two identical cases is a bug wherever it appears:

twins_check.kso
fn same x
  x

fn same y
  y
kanso check twins_check.kso
error[dispatch]: overlapping overloads of `same` are illegal
  --> twins_check.kso:4:4
   4 | fn same y
          ^

the ladder ranks the rungs against each other, but never two entries on the same rung—two typesets that overlap, two patterns that catch the same value. there is no tie-breaker by declaration order, because order is inert here: the compiler already knows which is more specific, and where it can't tell, it refuses the program and you decide.

if is for content, dispatch is for shape

none of this abolishes the ordinary yes-or-no decision. when the question is about a single value's content—is this number negative, is this age under thirteen—if is the right tool, and in kanso it is a plain function, if condition then_value else_value, an expression like any other. there is no statement form of if and no separate ternary:

admission.kso
fn admission age
  if (age < 13) "child ticket" "regular ticket"

print (admission 9) .> (_ -> print (admission 30))
output
child ticket
regular ticket

because if is an expression that returns a value, it nests where you need a third branch—here the sign of a number, three answers from two decisions about one integer:

sign.kso
print (sign (0 - 4)) .> (_ -> print (sign 0)) .> (_ -> print (sign 9))

fn sign n
  if (n < 0) "negative" (if (n > 0) "positive" "zero")
output
negative
zero
positive

a branch sometimes needs a working name on the way to its answer. binding is a statement, and statements need room, so the branch takes the layout form: the condition alone on the if line, else at the same indent, statements beneath each, and every branch ending in the value it answers with:

receipt.kso
print (total_line 12000) .> (_ -> print (total_line 800))

fn total_line spent
  if (10000 < spent)
    saved = spent / 10
    "big order: {spent} yen, {saved} back in points"
  else
    "thanks: {spent} yen"
output
big order: 12000 yen, 1200 back in points
thanks: 800 yen

the two spellings never compete, because the compiler holds the boundary: a branch with no bindings must use the expression form, and a branch that binds must use the block. there is no else if — a third case is a nested if or, nine times out of ten, the dispatch arms the next section argues for.

the line you never want to cross is using if to ask which kind of thing a value is. kanso answers that question with dispatch. suppose a chat log carries three kinds of event. the shape-question—"which event is this?"—is answered by three arms of one function, each unpacking the record it names:

events.kso
type joined
  who

type left
  who

type renamed
  after
  before

print (render (joined "clay"))
  .> (_ -> print (render (renamed "kanso" "kai")))
  .> (_ -> print (render (left "mugi")))

fn render (joined who)
  "{who} joined"

fn render (left who)
  "{who} left"

fn render (renamed after before)
  "{before} is now {after}"
output
clay joined
kai is now kanso
mugi left

a fourth kind of event means a fourth render arm, dropped in next to its siblings—and nothing else in the program changes. compare that to the switch-on-a-tag you would grow instead, one that every new case forces you to find and edit again, in every place it appears. with dispatch, everything a type does sits in the arms that name it. the rule: if is for a decision about one value's content; overloads are for a decision about a value's shape. when you catch yourself asking "which kind of thing is this?", write another arm.

enso-neko - sleeps in a circle she never quite closes
ensō-neko

types before functions, arms most-specific first, ascription tight as n:int, one blank line between declarations. everything has one place to go, so neither you nor the next reader has to decide where.

tsuru - folds your program until it is small enough to fly
tsuru

a note for later: those overload arms are not tag checks at runtime. i resolve dispatch statically, once per instantiation, and a group of literal or marker arms on one parameter collapses into a jump table. chapter 15 has the ir to prove it.

you can now model a domain the kanso way: each kind of thing is a record or a marker, a field that admits absence or variety is a typeset, and every "what kind is this?" is answered by an overload group rather than a conditional. you have seen construction and its two inverses, dispatch on literals, types, and markers, the specificity ladder with its rules on arm order and ambiguity, and the single rule that decides when to reach for if and when to reach for another arm. chapter 04 takes the one type we have kept offstage—err—and shows how the same dispatch machinery makes failure a value that handles itself.

exercises

  1. define a temperature record with a numeric field and a marker-typeset field that is either celsius or fahrenheit. write a to_celsius group of overloads that dispatches on the scale marker and returns the value in celsius—no if anywhere.
  2. write a label group over three markers weekday, weekend, and holiday, then add a fourth marker without touching any existing arm. confirm it compiles and runs, and note how many lines you had to change.
  3. take the sign function from this chapter and try to rewrite it as three overloads instead of nested if. where does dispatch help, and where does it fight you? write a sentence on why sign is a content question, not a shape one.
  4. deliberately trigger three of the chapter's compile errors on purpose: a keyed read whose fields are out of alphabetical order, overload arms in the wrong specificity order, and two arms dispatch cannot tell apart. read each diagnostic and write the one-line rule it is teaching.
  5. declare type meters int and type feet int, then write one describe group with an arm for each and a third for plain int. call it three ways and confirm each lands where you expect. then delete the meters arm and predict which arm answers meters 5 before you run it.