Bindings and refs
A binding connects a name to a storage cell, and a namespace is the table
of those cells. This page covers the surface first, var, def, let,
and set!, then the model underneath, where every name resolves through a
ref.
Defining names
var introduces a name in the current namespace:
var greeting "hello"
var count 3
print greeting count
At the top of a file the namespace is the module’s. Inside a function
body it is the function’s; a var there is local, as an assignment
in a Python function is.
def defines a function and binds it the same way:
def area [width height]:
* width height
area 6 7 ;; 42
var and define
var, def, global and syntax are the idiomatic binding forms.
The Scheme spellings remain for code written in that style, and the two
mix freely in one module:
Koskari |
Scheme |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
syntax inside a body scopes the transformer to the rest of that body;
global syntax and define-syntax bind it at the module once the form
has run.
define takes Scheme’s shape: a function’s name goes inside the pair
with its formals, as in Scheme’s (define (area w h) ...):
define [area width height]:
* width height
area 6 7 ;; 42
def takes formals and a body, never a plain value; def x 1 fails to
compile. var takes a value, never formals and a body. A pair after
define is always a function head, never a pattern: define [a b] x
defines a function a of one argument b, where var [a b] x unpacks
x into a and b.
Local names with let
let binds names for one body:
let [[width 6] [height 7]]:
(* width height)
Outside the body those names are not bound. A let inside a function
scopes a helper to one calculation without touching the function’s own
names.
Changing a binding
set! stores a new value into a name that is already bound:
var counter 0
set! counter (+ counter 1)
incr! and decr! are the same store for the two common updates. Inside
a function, set! on a name with no binding in any enclosing scope is a
compile error; declare the name first, with var for a local or
global var for a module name. At module level the store creates the module name.
An inner function stores into the outer name it finds, so Python’s
nonlocal has no counterpart:
def make-counter []:
var count 0
def tick []:
incr! count
count
tick
Shadowing
A local binding can reuse an outer name, and the inner one wins inside its body:
var message "outer"
let [[message "inner"]]:
print message ;; inner
print message ;; outer
Namespaces
A namespace is a lexical scope: a table of names, each tied to a ref.
Modules, let bodies, and function frames each have one, chained to the
namespace they were created in.
A namespace answers questions at three tiers, and its Python-facing API keeps them apart.
Tier |
What it holds |
Reached through |
|---|---|---|
1 |
builtins: the names a module falls through to |
|
2 |
refs: the names bound here, each to a ref |
|
3 |
values: what those refs currently hold |
the mapping protocol: |
Tier 2 is append-only. A name once bound is never unbound, only emptied,
because closures may already hold its ref. del ns[name] and
ns.clear(name) empty the ref; afterwards name in ns is false while
name in ns.refs stays true. ns.ref(name) on a module namespace
creates a ref when the name is missing, so a request for a ref is the
one read that creates. For a name the builtins hold, the ref starts out
holding that builtin and lives apart from the module’s bindings: a
function that reads max shares that ref, while "max" in ns,
ns.refs, ns.bound("max"), dir(module) and a star import do not
report it. The name becomes a module binding when the module binds it,
by a var or def, a store, a global in one of its functions, or an
attribute store on the module, and the same ref then carries the
module’s value to every holder. For any other missing name the new ref
is empty and bound at once. A builtin’s ref is the module’s own:
storing through it changes the name inside this module only, and later
changes to the builtins are not seen through it.
The mapping protocol never reaches tier 1. A builtin is not in a module
namespace, ns[name] raises KeyError for it, and ns.get(name)
returns the default. The lookup a symbol performs when evaluated, this
namespace’s value, then each enclosing namespace’s, then the module’s
builtins, is ns.resolve(name), with an optional default in place of
NameError.
A Module object’s attributes are tier 3 of its namespace: module.name
reads a value the module holds and dir(module) lists those names.
Builtins are not attributes of a module.
Ref
A ref is a mutable handle: deref reads the current value, store
writes, clear empties the slot. Refs can represent a local name, an
attribute, a subscript, a parameter, or other custom get and set logic.
Surface forms:
(ref sym), a ref to a variable (compile/ref)Reader
&nameand&obj.attr: an atom prefixed with&wraps it in(ref ...)Reader
&(item coll key):&immediately followed by(wraps the parenthesized form in(ref ...)(ref (attr obj name)),(ref (item coll key)), parameters, and so on
Bytecode VAR_REF pushes the ref for a name; attribute and item refs use
their own opcodes.
Bind and store
These are different operations.
Operation |
Meaning |
Compile form |
|---|---|---|
bind |
Attach a name to a ref, introducing or replacing the mapping |
|
store |
Write a value through the ref a name already uses |
|
Bind answers “which ref does this name resolve to?” Store answers “what value is in that ref now?” A name is bound before anything can be stored into it, and reassignment of an existing variable is a store, not a new bind.
The bind! macro can install
typed refs (item, attribute, dynamic, parameter, environ, and others)
while still using bind under the hood. The environ ref reads and writes
a process environment variable through os.environ; see
compile/define-environ.
Deref
Deref reads through a ref and yields the value.
(deref expr),compile/derefThe runtime
derefbuiltin, whenderefis wanted as a first-class function
Use ref when you need the cell, for example to pass a mutable slot to
another form. Use deref when you need the contents.
References and storage indirection
works through both, with bind! and by-reference parameters.
By-ref formals
A parameter declared with a leading & aliases the ref the caller
passes instead of receiving a copy of the value, so writing through it
with set! updates the caller:
def add-a [a &result]:
set! result (+ a 1)
var value 100
(add-a value &value)
value ;; 101
&result in the formals marks the parameter as by-ref. &value at the
call is the ref reader shorthand; any ref-valued expression works there,
such as (add-a value &(item xs 0)). By-ref is allowed on positional and
keyword parameters only, never on the * or ** parameters.
Assignment macros
set! expands to the right special:
Symbol assignment:
setq(store)Augmented symbol assignment
(set! x (+ x 1)):compile/inplaceIn-place item or attribute update:
compile/inplace-itemorcompile/inplace-attrvia theinplacemacroPlain item, attribute, or ref store:
set-item,set-attr, orset-ref(set! (values pattern ...) value):compile/setq-values
Unpack
Unpack splits one value across several names using a pattern: a list
shape with optional *: rest segments, the same family as for-each
bindings.
Introduce names:
compile/define-values,let-valuesStore into existing names:
compile/setq-values
var [x y] #(1 2)
set! (values a b) point
let-values [[u v] #(3 4)]:
(+ u v)
The compiler evaluates the right-hand side once and assigns each binder.
Vocabulary
Term |
One line |
|---|---|
bind |
name to ref mapping |
store |
value through an existing ref |
ref |
handle to a mutable location |
deref |
read through a ref |
resolve |
what a symbol sees: this namespace, its parents, then builtins |
unpack |
one value to many names through a pattern |