chapter 02

values and bindings

chapter 01 ended on a program that was one binding and one statement. a binding is a name, one space, =, one space, a value; an entry file is a run of such bindings and the statements that use them:

binding.kso
age = 47
price = 19.99
name = "clay"
print "{name} is {age}, owes {price}"
output
clay is 47, owes 19.99

the braces hold any expression, evaluated in place. put arithmetic between them and it runs:

interp.kso
guests = 4
print "table for {guests + 1}, please"
output
table for 5, please

most of a kanso program is names bound to values, and the values come in a small set of kinds. this chapter is a tour of those kinds—numbers, strings, lists, maps, records—and the two rules that govern the names you bind them to. every kind is a value, and every value is immutable. nothing in kanso mutates; a name may be pointed at a new value, but the old value is never edited in place.

numbers are exact

an int is a whole number, and its arithmetic is exact. multiply two large counts and you get the exact product—no silent wraparound into a small, wrong number the way a fixed-width int gives you in C:

ints.kso
per_day = 86400
days = 365000000
print "{per_day * days}"
output
31536000000000

exactness holds past the machine word. an int has no ceiling: while a number fits in 64 bits it is kept in one, and when an operation's answer would need more, the runtime moves the answer to a bignum that grows as far as the number does. every engine works this way, compiled code included, so this program prints all twenty-one digits:

overflow.kso
billion = 1000000000
print "{billion * billion * 100}"
kanso run overflow.kso
100000000000000000000

division by zero takes a different route. arithmetic has no answer to give, so the division answers a math_failure—an ordinary value whose text is the reason. print it and you see the reason; chapter 04 shows how an arm asks for it by name:

divzero.kso
n = 10
steeps = []
print "{n / length steeps}"
kanso run divzero.kso
division by zero

floats, and how they meet ints

a float is the ieee double you already know—written with a decimal point, and it always renders with one, so a float is never mistaken for an int on the page. when the two meet, an int divided by an int stays in integer arithmetic and discards the fractional part—7 / 2 is 3, not 3.5. but the moment a float enters the expression, the int widens to meet it, and the result is a float:

widen.kso
exact = 7 / 2
widened = 7 / 2.0
mixed = 3 + 0.5
print "int/int: {exact}, int/float: {widened}, mixed: {mixed}"
output
int/int: 3, int/float: 3.5, mixed: 3.5

two rules, both visible in one line. 7 / 2 keeps whole-number arithmetic and drops the remainder to give 3. 7 / 2.0 and 3 + 0.5 each mix the two types, so the int widens and the answer is a float, printed with its point. widening only ever runs one direction—int to float, never a float quietly rounded to an int—because that direction loses nothing. when you want the reverse, you ask for it by name: text/to_int, text/to_float, math/round, and math/sqrt are the conversions—imported from std modules you will meet properly in chapter 07—and each states exactly what it does to the value.

an int can also be written in hex or binary. 0xff and 0xFF are both 255, and 0b1010 is 10. the prefix is lower case, and hex digits may be either. a float can carry an exponent, as in 1e-8 or 6.02e23, and a literal with an e in it is always a float, so 1e3 is 1000.0. a minus touching any of these forms is part of the number, as it is in -3.

strings interpolate

strings are utf-8 text in double quotes. the one piece of machinery they carry is interpolation—{expression}—and because the braces take a full expression, you rarely need to concatenate anything. build the sentence you want directly:

strings.kso
name = "clay"
scoops = 3
print "{name} ordered {scoops} scoops, {scoops * 2} on the house"
output
clay ordered 3 scoops, 6 on the house

lists

a list is an ordered sequence, written with brackets and—like everything in kanso—no commas between the elements. the workhorse verbs live one import away in std/list: list/map transforms every element, list/select keeps the ones a predicate approves, list/sort orders them, and list/sum reduces them to a number. length is ambient—no import. indexing is a tight [i], and it is 1-based: xs[1] is the first element. the elements sit next to one another in memory, so xs[i] costs the same whether i is 1 or a million.

lists.kso
import "std/list"

xs = [3 1 2]
doubled = list/to_list (list/map xs (x -> x * 2))
small = list/to_list (list/select xs (x -> x < 3))
first = xs[1]
print "sorted {list/sort xs} doubled {doubled} small {small}"
  .> (_ -> print "sum {list/sum xs} len {length xs} first {first}")
output
sorted [1 2 3] doubled [6 2 4] small [1 2]
sum 6 len 3 first 3

the plain subscript asks: a miss answers none rather than failing. and a none is loud—interpolate it and it prints as <none>, so a miss you forgot to plan for shows up in the output instead of passing as a plausible value. when the index must be there, add the sigil—xs[i]!—and the read answers an effect: a box holding the element, which .> opens, and a miss is a failure that bubbles through the box to the endpoint (chapter 04 draws this line carefully). asking for the ninth element of a three-element list:

at_miss.kso
xs = [10 20 30]
print "{xs[9]}"
kanso run at_miss.kso
<none>

so one operation has two spellings. xs[i]! says the element is there and hands you a box to open; plain xs[i] says it might not be and hands you the value, or none. which one you write is a claim about your data, and the compiler holds you to it.

maps

a map associates keys with values. the literal is written in braces with a colon between each key and its value, { "clay":47 }, and lookup is the same tight [k] you use on lists. put returns a new map with one more entry—the original is untouched, because values do not mutate—and entries hands back the pairs in sorted key order, so iteration is deterministic every single run:

maps.kso
ages = { "clay":47 "sam":30 }
ages = put ages "mai" 8
print "clay is {ages["clay"]}" .> (_ -> print "all: {entries ages}")
output
clay is 47
all: [entry "clay" 47 entry "mai" 8 entry "sam" 30]

the entries came back clay, mai, sam—alphabetical, though mai was inserted last. a map's entries come back in key order, whatever order they went in. like a list, its lookup carries both spellings. the strict ages["nobody"]! promises the key exists, and a missing key is a failure bubbling from the box:

map_miss.kso
ages = { "clay":47 }
ages["nobody"]! .> (age -> print "{age}")
kanso run map_miss.kso
error[endpoint]: unhandled err reached the entry: "missing index "nobody""
  born in the entry at map_miss.kso:2

a list or map too long for one line is written one element to a line. the opening bracket ends the line that starts the literal, each element sits on a line of its own two spaces further in, and the closing bracket sits alone at the indent the literal started from. a map puts one key:value entry on each line:

long_list.kso
teas = [
  "sencha from the hills above shizuoka, steamed for thirty seconds"
  "gyokuro, grown in the shade for three weeks before the picking"
  "hojicha"
]
cups = {
  "a guest who came early and waited by the gate without complaint":2
  "sam":1
}
print "{length teas} teas" .> (_ -> print "{cups}")
kanso run long_list.kso
3 teas
{ "a guest who came early and waited by the gate without complaint":2 "sam":1 }

a list that fits on one line is written on one line. the compiler refuses the spanning form for it, for the same reason it refuses a needless continuation (appendix c).

records

when a value has named parts, give it a type. a type declaration lists its fields—each with a name and a type—in the order you choose, and that order becomes the shape you construct with. user 47 "clay" fills age then name, positionally, in the order the fields were declared:

records.kso
type user
  age
  name

clay = user 47 "clay"
user years who = clay
{ name:alias } = clay
print "{who} is {years}, also known as {alias}"
output
clay is 47, also known as clay

you get the parts back out by destructuring, and there are two ways to do it. the positional form, user years who = clay, binds every field at once by position—years to age, who to name. the field form, { name:alias } = clay, names just the field you want and binds it to a name of your choosing. when one field is all you want, a dot reads it: clay.name. chapter 03 says when to reach for each form.

a record is never changed after it is built, so a record with one field different is a new record. merge builds it: it takes the record and a map literal naming the fields to replace, and carries every other field over. the original is untouched:

older.kso
type user
  age
  name

clay = user 47 "clay"
older = merge clay { "age":48 }
print "{older}" .> (_ -> print "{clay}")
kanso play older.kso
older/user 48 "clay"
older/user 47 "clay"

each name in the map has to be a field of the record's type, and the compiler refuses one that is not. like construction, merge is allowed only in the module that declares the type.

equality and comparison

equality in kanso is structural: two values are equal when they are built the same way, all the way down. two lists with the same elements are equal; two records with the same fields are equal. kanso has no notion of object identity, so there is no second kind of equality to keep track of. comparison (<, <=, and their mirrors) works on numbers and orders strings lexicographically:

equality.kso
type point
  x
  y

nums = [1 2 3] == [1 2 3]
words = "apple" < "banana"
order = 5 <= 5
here = point 1 2
there = point 1 2
print "lists {nums}, words {words}, order {order}, points {here == there}"
output
lists true, words true, order true, points true

here and there are two separate constructions of the same point, and they compare equal, because equality looks at what a value contains. with immutable values there is nothing else for it to look at, and it is what lets a map use records as keys and lets two lists compare with a single ==.

floats follow one order on every engine. an int and a float compare as the numbers they are, so 0 == 0.0 holds. every NaN equals every other NaN and ranks above inf, however it was made, so a list holding a NaN equals itself and list/sort puts NaNs last. -0.0 equals 0.0 and sorts beside it, though it still prints as -0.0. a program that wants NaNs ordered another way wraps its floats in a type of its own and gives that type its own < arm.

rebinding

values are immutable, but a name may be rebound to a new value. this reads naturally when a value evolves through steps—each line is a fresh, exact value, and the name simply follows along:

rebind.kso
total = 10
total = total + 5
print "total: {total}"
output
total: 15

with one condition: each version of the name must actually be used before the next one replaces it. rebind over a value nobody read, and kanso asks why the first line exists at all:

rebind_check.kso
x = 1
x = 2
print "{x}"
kanso check rebind_check.kso
error[unused]: unused binding: each version of `x` is used before the next
  --> rebind_check.kso:1:1
   1 | x = 1
       ^

the rule is what lets the compiler know exactly when each value dies, which is what lets kanso free memory precisely with no garbage collector (chapter 15).

no name is ever shadowed

rebinding replaces a name within one scope. shadowing is reusing, inside a function, a name that already means something else outside it, and kanso refuses it. a local binding or a parameter may not take a name a top-level declaration already holds. the compiler says where the name is declared and asks you to rename the binding:

shadow_check.kso
tau = 5

fn area r
  tau = 3
  tau * r

pub play = print "{area 2} {tau}"
kanso check shadow_check.kso
error[name]: `tau` is already a declaration, at line 1; rename the binding
  --> shadow_check.kso:4:3
   4 |   tau = 3
         ^

so a top-level name means one thing everywhere in its file, and a reader never has to work out which tau is in view. like the rebind rule, it also keeps each value's lifetime unambiguous, which the memory model relies on to drop reference counts at compile time.

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

a value never changes under you; a name is just where you are looking. rebind all you like, as long as each version is read before the next replaces it. inside a function you cannot reuse a name the file already gave to something else. nothing i saw a moment ago has quietly become something else, so i sleep well.

mugi the tanuki - runs the interpreter; pays for everything in boba pearls
mugi

a lookup that misses, a division by zero: i hand you the failure as a value you can print. type a mistake on purpose once and read what comes back. it is the cheapest way to learn what kanso will and won't do.

what you can now do

you have the whole value vocabulary: exact ints with no ceiling, floats and the one-way widening that joins them, strings that interpolate any expression, lists and maps with strict lookup and a planned-for miss, records you construct positionally and take apart by destructuring, and structural equality that reaches all the way down. you have the two rules that govern names—rebind, but use each version; and never shadow a name already spoken for. with these you can model most of the data a real program touches, and you have met the failure rail—err and none riding to the endpoint—that chapter 04 makes the center of the story. next, chapter 03 turns the type you just met into the language's dispatch mechanism.

exercises

  1. build a map from three product names to their prices, then print the price of one product and the full list of entries. add a fourth product with put and print the entries again—note where it lands in the order.
  2. declare a type rectangle with height and width fields. construct one, destructure it positionally, and print its area. remember the fields construct in the order you declared them.
  3. start from a list of five numbers. in one program, print the list sorted, the list with every element doubled (list/map), the elements above a threshold (list/select), and the total (list/sum).
  4. reproduce the "unused binding" error on purpose: bind a name, rebind it without reading the first value, and run kanso check. then fix it two ways—once by using the first value, once by deleting the first line—and confirm both check clean.