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. get that one tool into your hands and every conditional you were about to write turns into a definition 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 are alphabetical—enforced, like everything else other languages leave to code review. 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 alphabetical order:

writing the type on a field is an error, not a style preference. 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.

a written type earns its place where it restricts below what use would permit, and that place is a type of its own. type state archived draft published says the set is closed at three, which no amount of reading the program can tell you. that one you write down.

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

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 = "…" outside the one construct in the next section, 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 gives the knot its own construct. inside a build block, and nowhere else, a field can be assigned after its record exists:

knot.kso
type person
  name
  partner

couple = build
  ada = person "ada" none
  bob = person "bob" ada
  ada.partner = bob
  ada
print "{couple.name} and {couple.partner.name}"
>> print "back where we started: {couple.partner.partner.name}"
output

none holds ada's place while she waits for bob, and the assignment on the fourth line closes the loop. when the block ends the whole group freezes: nothing in it can be assigned again. what leaves the block is ordinary immutable data that happens to contain a cycle, so you can walk the ring as far as you like—((couple.partner).partner) is ada again—and no code downstream has to defend against a half-built record, because a half-built record never leaves.

the freeze buys two things. it is what lets the type say partner:person instead of some nullable stand-in, and it is what keeps the memory model simple, which ch10 returns to: everything a block 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—we'll get there—typeset, then the unannotated generic that catches everything left. note the ascription is tight: n:int, no spaces. n: int is a formatting error, because the loose spelling is reserved for something the compiler will use later. 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, for free—no enum keyword, no special case, just types and the switch you already have:

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 run 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 run 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, that is your bug to resolve, not a coin it will flip for you.

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. no statement form, no dangling else, no ternary syntax to argue about:

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. that question has a better answer, and it is the design center of the whole language. 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. the conditional scatters a type's behavior across the codebase; dispatch gathers it onto the type. the rule of taste is short: 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?", the answer is never a conditional—it is another arm.

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

fields alphabetical, arms most-specific first, ascription tight as n:int, one blank line between declarations. there is exactly one place everything goes, which means you never have to decide—and neither does the person reading after you.

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 10 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 and the two laws that keep it honest, 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: fields 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.