References and storage indirection
A name is attached to a storage location, and the location itself can be taken hold of. Koskari calls that handle a ref. A ref is a pointer with the sharp edges removed: it points at one storage location, there is no arithmetic on it, and it is never null or dangling.
Bindings and refs is the vocabulary page – bind against store, the ref surface forms, by-ref formals. This page is about what the indirection buys you.
The snippets here are also examples/refs.ki in the repository.
A name labels a location
var creates a storage location and attaches a name to it. set!
changes what is inside the location; the name does not move.
(var food 10)
(set! food 11)
food
; -> 11
(ref food), or its reader shorthand &food, hands you the location
rather than its contents:
(var r &food)
(type r)
; -> <class 'koskari.lib.Ref'>
(deref r)
; -> 11
r is one step removed from the value. food gives the value directly;
r gives the location, and (deref r) follows it.
Reading and writing through a ref
Assigning to the deref stores through the ref into the location it points at:
(set! (deref r) 20)
food
; -> 20
The line that changed food never mentioned food. Under the hood this
dispatches to the ref’s store method, so (r.store 20) does the same
thing when you already hold the ref.
The target of set! decides which of the two happens. (set! (deref r) v) writes through the ref; (set! r v) assigns to the name r,
discarding the ref:
(set! r 99)
food
; -> 20 (unchanged)
r
; -> 99 (r no longer holds a ref)
Locations with no name
A ref can point at a list slot or an object attribute, neither of which has a variable name:
(var xs #[1 2 3 4])
(var cell &(item xs 2))
(deref cell)
; -> 3
(set! (deref cell) 100)
xs
; -> [1, 2, 100, 4]
&obj.count is the same move on an attribute. This is where “storage
location” earns its name: cell reads and writes a slot that no name
labels.
bind! makes the indirection invisible
bind! attaches a name to a ref as that name’s storage, so plain reads
deref and plain set! stores through:
(var total 5)
(bind! alias &total)
alias
; -> 5
(set! alias 42)
total
; -> 42
That works for the unnamed locations too, which is the payoff: aim a name at a slot once, and from then on the code reads like ordinary variable access while writing back to the original slot.
(bind! head &(item xs 0))
(set! head 1000)
xs
; -> [1000, 2, 100, 4]
A by-ref formal, [&n], does this bind! for the caller’s ref
automatically; see
by-ref formals. Passing a
non-ref to one is an error at the call, not a silent corruption:
def bump [&n]:
set! n (+ n 1)
(bump 5)
; -> TypeError: bump() by-ref formal n requires a Ref, not 5
take!
take! reads a location and
clears it in one step, returning the value that was there:
(var y 5)
(take! y) ;; 5; y is now empty
(var d #{"j": 5})
(take! d["j"]) ;; 5
d ;; {}
Reading a cleared name afterwards raises
NameError: Empty reference: y. Use take! when
handing ownership of a value onward, so nothing reads the old location by
accident.
Dynamic refs
The indirection goes one step further than a slot. A dynamic ref supplies its own get, set, and clear behaviour, so there is no underlying storage at all – reading the name runs a function:
(var store #{"temp": 0})
(var log #[])
(bind! sensor (dynamic { (begin (log.append "read") store["temp"]) }
{ (set! store["temp"] %1) }))
sensor ; -> 0 (runs the getter)
(set! sensor 7)
store ; -> {'temp': 7}
log ; -> ['read']
The two fences are the getter and the setter; the setter receives the new
value as %1. Optional third and fourth fences supply the clear and the
emptiness test. bind! takes the same position for constant,
memoized, parameter, and environ refs, which each compute their
value rather than hold it.
Compared with Python
Python names already reference objects, so two names can see the same
mutable list. You cannot get a reference to a binding, though, so a callee
can never rebind a caller’s variable. Koskari lets you pass the location
itself, so a function can write back to the caller’s a, a list slot, or
an attribute. Python passes object references by value; Koskari can pass
the slot.
See also
Bindings and refs: bind against store, deref, namespaces, by-ref formals