Implicit iteration
One of the most common qbie errors is to specify iteration unnecessarily.
Many of us have had our brains trained by other programming languages to loop through data structures. It’s close to a reflex.
One potato, two potato.
And this is a problem? Not at all. Exactly what you should do in C-style programming languages. You can probably write for-loops in your sleep.
You can probably write for-loops in your sleep.
You can probably write for-loops in your sleep.
We need you to wake up.
Waking up
Zen master Zuigan called out to himself each day.1
— Master!
— Yes, sir.
— Become sober.
— Yes, sir.
— Do not be deceived by others.
— Yes, sir; yes, sir.
First off, most binary operators iterate implicitly.
Scalar extension
No doubt you are familiar with examples such as
q)10 20 30 + 3 4 5
13 24 35
q)1000 + 3 4 5
1003 1004 1005
In the first, Add is applied between corresponding items of the two vectors. Two same-length vectors conform, and all is well.
In the second, atom 1000 is added to each item of the vector 3 4 5
.
The conformists
Two data structures conform if
- they are lists with the same number of items and corresponding items conform; or
- one or both of them is an atom
This definition recurses. So let’s begin with the terminating case, when one is an atom.
The latter is sometimes described as ‘scalar extension’: an atom (scalar in the terms of an ancestor language) gets paired with every item of a list. So the following are identities:
3 4 5+1000
3 4 5+1000 1000 1000
That is true as far as it goes, but there is a good deal more going on here.
Atomic iteration
In atomic iteration scalar extension is applied recursively until atoms are reached.
q)L:(1 2 3;(4 5;(6;7 8));9;(10 11;12))
q)L+1000
1001 1002 1003
(1004 1005;(1006;1007 1008))
1009
(1010 1011;1012)
Since 1000 is an atom it conforms to any list, so winds up added to every atom in L
.
You could think of atomic iteration here as an atomiser, spraying a cloud of atoms that settle onto every leaf of the L
tree but the oversimplification obscures how atomic iteration actually works.
q)L+1000 2000 3000 4000
1001 1002 1003
(2004 2005;(2006;2007 2008))
3009
(4010 4011;4012)
Above we see the four items of L
paired with the four ints.
- 1000 adds to each of
1 2 3
- 2000 adds to each of
4 5
; to6
; to each of7 8
- 3000 adds to
9
- 4000 adds to each of
10 11
; to12
q)L*(10;(10b);100b;1000)
10 20 30
(4 5;(0;0 0))
9 0 0
(10000 11000;12000)
Above
- 10 multiplies
1 2 3
1b
multiplies4 5
;0b
multiplies6
then7 8
100b
multiplies9
- 1000 multiplies each of
10 11
then12
Notice that in atomic iteration if one argument is an atom or vector the result mirrors the structure of the other argument.
But in the general case, as immediately above, you can count only on the top-level structure being replicated. That is, the result will have as many items as the argument list/s.
It will also conform to both.
Which operators and keywords iterate atomically?
- Arithmetic operators
+
,-
,*
,%
- Comparison operators
=
,<>
,<
,<=
,>=
,>
- Logical operators
|
,&
,or
andand
- Math keywords:
abs
,acos
,asin
,atan
,ceiling
,cos
,div
,exp
,floor
,log
,mod
,neg
,rand
,reciprocal
,signum
,sin
,sqrt
,tan
,xexp
,xlog
null
and Fill^
- Index At
@
- Cast and Tok
$
,string
upper
,lower
Some of these will be more familiar than others.
q)ceiling L*1.5
2 3 5
(6 8;(9;11 12))
14
(15 17;18)
q)acos neg (11b;(1b;(111b;11b);11b);1b)
3.141593 3.141593
(3.141593;(3.141593 3.141593 3.141593;3.141593 3.141593);3.141593 3.141593)
3.141593
q)(3 4;5)xexp(1;2 3)
3 4
25 125
The most common use case for Fill uses scalar extension to replace every null value with some other value, such as zero.
q)0^1 2 3 0N 5 6 0N 8 9 / zero for null
1 2 3 0 5 6 0 8 9
q)"-"^"string with spaces"
"string-with-spaces"
But Fill iterates – atomically.
q)"one slick brown dog"^"the quick fox"
"the quick brown fox"
q)N:(1 0N 3;(4 5;(6;0N 8));9;(10 0N;12))
q)1000^N
1 1000 3
(4 5;(6;1000 8))
9
(10 1000;12)
q)1000 2000 3000 4000^N
1 1000 3
(4 5;(6;2000 8))
9
(10 4000;12)
Cast:
q)"hij"$101b
1h
0i
1
q)"bhij"$L
111b
(4 5h;(6h;7 8h))
9i
(10 11;12)
q)s
("The";"quick")
(("Brown";"Fox");"JuMpS")
"over"
(("the";"LAZY");"dog")
q)lower s
("the";"quick")
(("brown";"fox");"jumps")
"over"
(("the";"lazy");"dog")
There are also some variants on atomic indexing.
Index At
Index At is right-atomic – atomic only in its right domain.
q)"abcdefghi"@L
"bcd"
("ef";("g";"hi"))
" "
(" ";" ")
Tok
The right domain of Tok is strings. (We could say it treats atoms in its right domains as 1-item strings.) So recursion in its right domain stops at strings.
We could call Tok right string-atomic.
q)("B";"H";("I";"J");"F")$("123";"45";("6";"78");"999")
0b
45h
(6i;78)
999f
Nothing like like
The like
keyword has its own kind of implicit iteration. Its left domain is either a string or a list of strings, according to which it returns a boolean atom or vector.
q)"brown"like"br?wn"
1b
q)("brown";"brawn";"frown";"brain")like"br?wn"
1100b
Not left string-atomic
like
could be called left string-atomic if only it recursed through deeper structures – but it doesn’t.
q)string[(`brown;`brawn`frown;`brain)]like"br?wn"
'type
[0] string[(`brown;`brawn`frown;`brain)]like"br?wn"
^
Nothing iterates like like
!
Aggregators
Aggregators iterate through the primary dimension of a list.
q)sum 3 5 8 13 /items of a vector
29
q)sum (3 5 8 13;2 4 6 8;1 3 5 7) /rows of a matrix
6 12 19 28
The sum
keyword covers the derived function +/
Add Over, which is the map-reduce pattern.
A matrix is a list of same-length vectors.
In applying Add between list items (rows) the rules of conformity hold, so sum
can be applied to a nested list of conforming items.
q)sum (3 5 8 13;3;1 3 5 7)
7 11 16 23
q)sum (3 4;(5;6 7);(8 9;10))
16 17
20 21
The last example proceeds by adding 3 4
to (5;6 7)
to get (8;10 11)
then adds (8 9;10)
to get (16 17;20 21)
.
From a rectangular array of rank \(N\) an aggregator returns a rectangular array of rank \(N-1\). Put another way, it reduces (removes) the first dimension of its argument.
Exercises
No peeking.
Attempt each exercise before looking at its answer.
The exercises clarify your thinking. Reading answers does not.
-
Define two nested lists
A
andB
such thatA+B
is a matrix.Answer
q)A:(1;2 3;4) q)B:(5 6;7;8 9) q)A+B 6 7 9 10 12 13
-
With a little care you can write utility functions that iterate implicitly. Write a binary function
to
that returns the inclusive range between its integer arguments, e.g.q)to[3;7] 3 4 5 6 7
Answer
to:{x+til 1+y-x}
-
Get
to
to work with conformable arguments; e.g.q)to[3;5 8] 3 4 5 3 4 5 6 7 8 q)to[3 4;5 8] 3 4 5 4 5 6 7 8
Answer
to:{x+til each 1+y-x}
-
Get
to
to iterate atomically.q)to[(3;4 5);(6 7;8)] 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8
Answer
q)to:{x+til each 1+y-x}' q)to[3;7] 3 4 5 6 7 q)to[3 4;5 8] 3 4 5 4 5 6 7 8 q)to[(3;4 5);(6 7;8)] 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8
-
From Paul Reps, Zen Flesh, Zen Bones ↩