chapter 02

values and bindings

chapter 01 ended on a program that was one binding and one statement. that is the whole of it. 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

that is the whole shape of the language: names bound to values, values built from 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 is a promise kanso keeps even when it cannot fit the answer. the language's design specifies arbitrary-precision integers—no ceiling at all—and the bignum backing that delivers it is on the way. the build you have today backs int with 64 bits, and when a result would not fit, it refuses to lie about it. instead of wrapping, it hands you a diagnostic:

overflow.kso
billion = 1000000000
print "{billion * billion * 100}"
kanso run overflow.kso
error[runtime]: integer overflow (int64 native build; spec int is arbitrary precision)

division by zero is handled the same way. there is no answer to give, so the failure becomes a value that travels to the program's endpoint (chapter 04 is entirely about this rail):

divzero.kso
n = 10
steeps = []
print "{n / length steeps}"
kanso run divzero.kso
error[endpoint]: unhandled err reached the entry: "division by zero"
  born in the entry at divzero.kso:3

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. what happens when the two meet is worth setting out. 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.

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.

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 a miss becomes an err that rides 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>

one operation with two spellings. xs[i]! says the element is there; plain xs[i] says it might not be. 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 reuses the bracket form with a colon between each key and its value, 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 in kanso has no incidental order to accidentally depend on. and like a list, its lookup carries both spellings. the strict ages["nobody"]! promises the key exists, and a broken promise is an err:

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

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 alphabetical order, 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. there is no clay.name dotted access—the . is the pipe operator (chapter 06), so reaching into a record is always a destructure, stated plainly.

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—no notion of identity, no "same object" versus "equal object" distinction to trip over. 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 asks what a value is, never where it lives. immutable values make this the only sensible answer, and it is the answer that lets maps key on records and lists compare in a single ==.

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 10). a value written and never read was never needed, and the compiler says so.

no name is ever shadowed

rebinding replaces a name within one scope. shadowing—quietly reusing a name that already means something else—is a different thing, and kanso does not allow it. a local binding may not take a name already claimed by a top-level declaration; the compiler tells you the name is spoken for and asks you to pick another:

shadow_check.kso
tau = 5
tau = 3

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

the payoff is that a name means one thing in the place you read it. no reader ever has to ask which tau is in view, and—like the rebind rule—no-shadowing keeps each value's lifetime unambiguous, which is exactly what the memory model needs to elide 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—each version must earn its keep first—but you cannot point two names at one meaning, or one name at two. i sleep soundly because nothing i saw a moment ago has quietly become something else.

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

a lookup that misses, a sum that won't fit—i hand you the failure as a value, not a lie. type the mistake on purpose once and read what comes back. it is the cheapest way to learn what kanso refuses to fake.

what you can now do

you have the whole value vocabulary: exact ints and honest overflow, 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 alphabetical order.
  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.