Skip to content

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 local
A+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 and set 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.

  1. 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

    q).math.aoc:{.math.PI*x*x} / area of circle radius x
    
    Above, an unqualified reference to PI in the lambda would refer to PI (undefined) in its definition context, the default namespace – and fail. Instead, the fully-qualified name is unambiguous.

    Alternatively

    \d .math
    PI:acos -1
    aoc:{PI*x*x}
    
    Above, PI is defined in the lambda’s definition context. This is more legible, provided you can see from the source above that the reference in aoc to PI is to .math.PI – which is not evident from the lambda’s display form.
    q).math.aoc
    {PI*x*x}
    
    Or
    q).math.aoc:.math.PI* {x*x} :: / composition
    q).math.aoc
    3.141593{x*x}
    
    The composition binds the value of .math.PI (at the time the composition is formed), not a reference to it.

  2. Lambda .util.foo should read environment variable PARM in whichever context it is called. How can it do that?

    Answer

    .util.foo:{[arg]
      parm:get`PARM;
      ..}
    
    Above, .util.foo reads the value of variable PARM in the current context.

    Better functional style would be to pass the value as an argument.

    .util.foo:{[parm;arg]
      ..}
    
    Or, with multiple environment variables, to pass them as a dictionary.
    .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 for get and set.

    Consider using fully qualified names instead.