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.
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}")
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:
type track
artist
minutes
title
song = track "fishmans" 6 "long season"
{ artist:who title } = song
print "{title}, by {who}"
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:
type track
artist
minutes
title
pub play =
song = track "fishmans" 6 "long season"
track artist _ title = song
print "{title}, by {artist}"
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:
type track
artist
minutes
title
fn credit (track artist _ title)
"{title}—{artist}"
song = track "fishmans" 6 "long season"
print (credit song)
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:
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}")
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:
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))
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:
fn fact 0
1
fn fact n
n * fact (n - 1)
answer = fact 20
print "20! = {answer}"
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:
type null
fn describe null
"nothing here"
fn describe x
"got {x}"
print (describe null) .> (_ -> print (describe 5))
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:
type null
pub play =
x = null 5
print "{x}"
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:
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))
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:
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}"
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:
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"}")
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:
fn fact n
n * fact (n - 1)
fn fact 0
1
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:
fn same x
x
fn same y
y
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:
fn admission age
if (age < 13) "child ticket" "regular ticket"
print (admission 9) .> (_ -> print (admission 30))
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:
print (sign (0 - 4)) .> (_ -> print (sign 0)) .> (_ -> print (sign 9))
fn sign n
if (n < 0) "negative" (if (n > 0) "positive" "zero")
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:
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"
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:
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}"
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.
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.
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
- define a
temperaturerecord with a numeric field and a marker-typeset field that is eithercelsiusorfahrenheit. write ato_celsiusgroup of overloads that dispatches on the scale marker and returns the value in celsius—noifanywhere. - write a
labelgroup over three markersweekday,weekend, andholiday, then add a fourth marker without touching any existing arm. confirm it compiles and runs, and note how many lines you had to change. - take the
signfunction from this chapter and try to rewrite it as three overloads instead of nestedif. where does dispatch help, and where does it fight you? write a sentence on whysignis a content question, not a shape one. - 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.
- declare
type meters intandtype feet int, then write onedescribegroup with an arm for each and a third for plainint. call it three ways and confirm each lands where you expect. then delete themetersarm and predict which arm answersmeters 5before you run it.