Name scope
How names are resolved within lambdas
Assign
The Assign operator :
names a value.
a:42
The operator is syntactically anomalous. Its left argument must be a name, which in the immediate context might or might not already be bound to a value.
Given an undefined left argument any other operator signals an error. Not Assign.
Assign is an operator.
Not just a syntactic token:
q)type(:)
102h
That is to say, the Assign operator has upsert semantics.
Above, a
is assigned to 42 whether it was previously assigned or not.
Just as if the context were a dictionary.
How long should a name be?
At the 2015 Iverson College meeting Arthur Whitney demonstrated a simple text editor written in four lines of k. A non-array programmer asked whether the code would not be more readable if the 1-character variable names were replaced by long, descriptive names. Whitney’s answer was characteristically terse:
— No.
Chris Burke offered a longer answer. Terse APL and k expressions get a lot done, and short names help us see the transformations.
One-character names often suffice in short lambdas.
Especially where arguments are vectors or atoms the default argument names of x
, y
and z
are all you need, and avoid cognitive clutter.
In a longer lambda, or one which interacts with other lambdas on non-primitive data structures, longer names help.
But avoid names that engage a reader’s natural-language processing.
thingstoprocess: foo bar N / AVOID LONG NAMES
Instead, keep it math-y. Use acronyms with a comment.
ur: foo bar N / user request
myfun:{[ttp;opts] / things to process; options dict
wiw:foo bar ttt; / what i want
wyw:fubar opts`request; / what you want
.. }
Context
A q expression is evaluated in context. A context is a namespace: like a dictionary in which the names of objects are keys, and a name can have only one value.
But there are other namespaces, in which the same name could have a different value.
Namespaces have names that begin with a dot.
The name of the default namespace is just a dot.
The system command \d
switches the current context between namespaces.
q)\d .util / switch to
q.util)PI:acos -1
q.util)PI / defined here
3.141593
q.util)\d . / switch to default namespace
q)PI / not defined here
'PI
Above, the system command creates the .util
namespace if it does not already exist.
Fully-qualified names
A fully-qualified name begins with a period.
.math.PI: acos -1
Upsert semantics again: the expression above creates a namespace .math
if it did not already exist.
Child namespaces
Namespaces may have child namespaces.
.math.trig.aoc: .math.PI* {x*x} :: / area of circle
Dot notation navigates the namespace tree.
q).a.b.c.foo:42
q).a.b.c.bar:666
q).a.b.c.foo*.a.b.c.bar
27972
Fully qualified names start with a period, resolve without ambiguity, and cannot be masked.
Namespaces
The interpreter ships with some namespaces populated:
├── . / default namespace
├── .h / HTML
├── .j / JSON
├── .Q / system utilities
└── .z / system, callbacks
The system namespaces are siblings of the default namespace, not its children. They inherit nothing from it.
You can add your own top-level namespaces. They can have child namespaces. For example:
├── . / default context
├── .app / application
├── .auth / authentication
├── .h / HTML
├── .j / JSON
├── .Q / system utilities
├── .math / math
│ └── .trig / trigonometry
└── .z / system calls, callbacks
KX reserves ALL single-character namespace names for its own use.
Do NOT use single-character names for your own namespaces.
Do NOT define objects within the system namespaces.
A later version of the interpreter might silently overwrite your definitions.
Current context
Except in a lambda (see below) unqualified names in q expressions get resolved within the current context.
System command \d
lets you set the current context to a top-level namespace.
q)A:99;B:100
q)A+B
199
q).ns1.A:1
q)\d .ns1
q.ns1)A
1
The current context can only be a top-level namespace.
q)\d `.a.b.c
'`.a.b.c
[0] \d `.a.b.c
^
Contexts are dictionaries
In fact, q represents contexts as dictionaries: a set of name-value pairs.
q)a:42
q)b:666
q)c:1729
The default context is the default namespace, represented by a dot.
q)key `.
`a`b`c
q)value `.
a| 42
b| 666
c| 1729
q)key `.j
``e`q`s`es`J`k`jd`j
Partly qualified names
In a top-level namespace you can resolve partly qualified names of its children.
q)\d .a
q.a)\v
`b`that`this
q.a)b.c.foo+b.c.bar
708
So code in top-level namespace with child namespaces can be written in a way that allows the top-level namespace to be renamed.
Name scope in lambdas
TL;DR
In a lambda, use fully qualified names for everything except arguments and local variables.
Strictly local
Arguments and unqualified names assigned in the lambda have strictly local scope.
- Strictly local scope
-
Variables can be read and set only by expressions in the lambda. Assignments persist only while the lambda is on the evaluation stack.
Definition context
- Definition context
-
The context in which the lambda was defined.
If not strictly local, a lambda resolves an unqualified name in its DEFINITION context.
This is not arbitrary.
q)pi:acos -1 / pi
q){pi*x*x} 1 / area of circle radius 1
3.141593
q).math.aoc:{pi*x*x} / area of circle radius x
Above, {pi*x*x} 1
refers to pi
in the default namespace.
The reference is not changed by assigning the lambda to .math.aoc
.
The same rule governs external assignments.
q)B:.ns1.B:.ns2.B:0 / initialise
q)(B;.ns1.B;.ns2.B)
0 0 0
q).ns1.foo:{B::A:x;}
q)\d .ns2
q.ns2).ns1.foo 3 / evaluate in .ns2
q.ns2)\d .
q)(B;.ns1.B;.ns2.B)
3 0 0
Above, the lambda was defined in the default namespace before being assigned to .ns1
and evaluated within .ns2
.
The value of B
was amended in the lambda’s definition context; i.e. the default namespace.
Get and set
In contrast, unqualified name arguments of get
and set
resolve in the current context.
q)C:.ns1.C:.ns2.C:0 / initialise
q)(C;.ns1.C;.ns2.C)
0 0 0
q)\d .ns1 / definition context
q.ns1)foo:{`C set 1+C::1+C:x;}
q.ns1)\d .ns2 / current context
q.ns2).ns1.foo 99
q.ns2)\d .
q)(C;.ns1.C;.ns2.C)
0 100 101
Above, the lambda
- assigns
C
to its argument 99;C
is a strictly local variable and does not persist after the lambda has been evaluated - adds 1 and makes an external assignment
::
of 100 in its definition context:.ns1.C
- adds 1 again and uses
set
to assign 101 in its current context:.ns2.C
The value of C
in the default namespace is unchanged.
Here are the contexts where an unqualified external name A
is resolved in the lambda.
(A direct read of A
resolves first in the lambda context; if not there, in the definition context.)
read | write | context |
---|---|---|
arg or localA+42 |
A:42 |
strictly local |
A+42 |
A::42 |
definition |
get `A |
`A set |
current |
Global variables
Global constants and variables are intended to be accessible to any expression in your application.
A variable in the default namespace
- does not have a fully qualified name
- cannot be read directly (i.e. as an unqualified name) in any other context
- can be read directly or set with Assign External
::
by a lambda defined in the default namespace - can be read or set directly with
get
andset
by a lambda evaluated in the default namespace
You will not wish to burden the reader of your code with such subtle distinctions.
Define a namespace for global variables and refer to them with fully qualified names.
You do not need get
to read the value of a fully qualified name, and you do not need set
or Assign External to set it.
Composition
Composition binds the values of objects (lambdas, variables) as they are when composed. Subsequent changes to the objects do not affect the composition.
q)PI:acos -1
q)sqr:{x*x}
q)aocc: PI* sqr :: / area of circle (composition)
q)aocc 5
78.53982
q)PI+:1
q)aocc 5 / ignores change to PI
78.53982
Evaluation of a lambda reflects the current value of any object it refers to.
q)aocl:{PI*sqr x} / area of circle (lambda)
q)aocl 5
103.5398
q)PI:acos -1
q)aocl 5 / reflects change to PI
78.53982
Exercises
No peeking.
Attempt each exercise before looking at its answer.
The exercises clarify your thinking. Reading answers does not.
-
Given
.math.PI:acos -1
define.math.aoc
to return the area of a circle (\(\pi r^2\)) from its radius argument when evaluated in the default namespace, e.g.q).math.aoc 1 3.141593
Answer
Above, an unqualified reference toq).math.aoc:{.math.PI*x*x} / area of circle radius x
PI
in the lambda would refer toPI
(undefined) in its definition context, the default namespace – and fail. Instead, the fully-qualified name is unambiguous.Alternatively
Above,\d .math PI:acos -1 aoc:{PI*x*x}
PI
is defined in the lambda’s definition context. This is more legible, provided you can see from the source above that the reference inaoc
toPI
is to.math.PI
– which is not evident from the lambda’s display form.Orq).math.aoc {PI*x*x}
The composition binds the value ofq).math.aoc:.math.PI* {x*x} :: / composition q).math.aoc 3.141593{x*x}
.math.PI
(at the time the composition is formed), not a reference to it. -
Lambda
.util.foo
should read environment variablePARM
in whichever context it is called. How can it do that?Answer
Above,.util.foo:{[arg] parm:get`PARM; ..}
.util.foo
reads the value of variablePARM
in the current context.Better functional style would be to pass the value as an argument.
Or, with multiple environment variables, to pass them as a dictionary..util.foo:{[parm;arg] ..}
.util.foo:{[env;arg] parm:env`PARM; ..}
Prefer fully qualified names
When writing or reading non-local variables, there are not many good use cases for Assign External
::
, nor forget
andset
.Consider using fully qualified names instead.