Operators & Nested Arrays
Vector 2:1, page 117
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 117
Abstract¶
Second generation APLs such as Dyalog and IBM’s APL2 allow two conceptually simple extensions to the APL language. Arrays may contain further arrays as elements; an operator may combine with any function to produce a derived function. This paper discusses the power which these extensions add to the language.
Nested or enclosed arrays, and the extension of the domain of operators ‘invent’ themselves quite naturally in APL. People — particularly those learning APL, will often type expressions which seem reasonable but which give errors. If, for instance
works, why doesn’t
Similarly, if
works, why doesn’t
We can draw an analogy between nested arrays in APL and ‘imaginary numbers’ in mathematics. If the polynominal x² − 1 = 0 has solutions then why doesn’t x² + 1 = 0? The introduction of ‘i’ the imaginary square root of ¯1 brought a completeness to this area of maths and found immense application in a wide range of very practical fields. So nested arrays bring a completeness and power to APL.
APL has always had recursive functions — now we have recursive data structures to complement them. Many problems have a natural expression in nested structures, for instance, representation of hierarchy in organisations or modelling of products made from sub-assemblies. These problems have in the past been modelled with function structures which match those of the problem, but with at best, clumsy data representation.
Enclosed arrays may ‘invent’ themselves in many ways in APL. As one example, if we type ,/1 2 3 what does the result look like? Firstly, it must be a scalar because the reduction of a vector reduces the rank by one, and secondly, the value of this scalar must be the concatenation of the numbers 1, 2 and 3. We have a scalar whose single value is a vector — an enclosed vector. Similarly, the catenate reduction of a matrix would produce a vector of enclosed vectors.
From the introduction of this single new data type many of the properties follow. If we ‘comma reduce’ a vector of vectors, we produce a single scalar, whose element is a vector, each of whose elements is a vector — arrays may be nested to arbitrary depths. Our vector of vectors may be: reshaped; catenated with other conforming arrays; rotated; and in fact, subjected to all of the structure-primitive functions. Using partial specification of the form
we can replace any element of an array with any other array. Nested arrays may therefore have arbitrary shape, rank and depth. Of course, individual implementations enforce limits on depth, rank and shape of arrays, but these limits are usually no more of a hindrance to working than their equivalents in first generation APLs.
In particular, a special case in an array all of whose elements happen to be scalars — these are the simple arrays of first generation APLs, with one extension — we no longer have the constraint that all of the elements have to be of the same type — all numbers or all characters — it is now perfectly valid to type
to form a simple six element vector — we can do arithmetic with the first 3 elements
but get a DOMAIN error if we include character elements
— each element of an array has its own property of type (numeric or character) rank and shape.
How should the scalar functions act on our now nore general concept of an array? If A is ,/1 2 3, what is 10 + A. The result of adding scalar 10 to scalar A must yield a scalar, and the most reasonable guess (which is in fact the case) for its value, is the scalar whose element is vector 11 12 13.
What about (1 2 3)4 + 10(20 30 40)? This must mean the addition of two two-element vectors — the juxtaposition of two arrays without an intervening function has always signified a vector although in the past this was only meaningful for scalar literals of the same type. From the rules of scalar conformability, the result must be a two element vector whose first element is (1 2 3) + 10 and whose second is 4 + (20 30 40) so the result must be 11 12 13 24 34 44.
So the scalar functions ‘pervade’ to any depth of array, providing that at each level, the arrays are scalar conformable.
Operators¶
In order to give examples, first let’s see a new primitive operator called “each”. “Each” takes its functions and applies it to (or between) each element of its argument(s) following the rules of scalar conformability.
The “each” operator appears so powerful and natural that ALL of the second generation APLs have included it as a primitive.
Now let’s look at the other implications of being able to do,/1 2 3. The advent of nested arrays enables us to extend the domain of operators. In first generation APLs, operators could represent results only for functions which produced scalar results from scalar arguments. Now that this restriction is lifted operators may apply ANY functions.
Defined Functions¶
∇ Z←A ADD B
[1] Z←A+B
[2] A,'+',B,'→',Z
∇
3 ADD 4
3 + 4 → 7
7
ADD/1 2 3 4
3 + 4 → 7
2 + 7 → 9
1 + 9 → 10
10
If, for instance, PERSONNEL were a vector of information — one element per person — and PRINT, a defined function to print information for one person:
Then
would print the information for each person.
Derived Functions¶
Operators combine with functions to form derived functions, these in turn may be applied by operators.
A←((1 1)(2 2)(3 3)) ((4 4 4)(5 5 5))
⍴A
2
⍴¨A
3 2
⍴¨¨A
2 2 2 3 3
+/¨¨A
2 4 6 12 15
ADD/¨A
2 2 + 3 3 → 5 5
1 1 + 5 5 → 6 6
4 4 4 + 5 5 5 → 9 9 9
6 6 9 9 9
Conclusion¶
Already we see examples of pieces of code contrasting first and second generation APL, in much the same way as we used to see examples contrasting APL and BASIC or PASCAL, for example:
∇ LIST1;NL
[1] ⍝ VECTOR REPRESENTATION OF ALL FUNCTIONS
[2] NL←⎕NL 3
[3] L:→(0∊1↑⍴NL)/0
[4] ⎕VR NL[⎕IO;]
[5] NL← 1 0 ↓NL
[6] →L
∇
∇ LIST2
[1] ⍝ VECTOR REPRESENTATION OF ALL FUNCTIONS
[2] ⎕∘←∘⎕VR¨↓⎕NL 3
∇
Nested arrays and extended operators have been available in APL for only a short time, but the increased power that they bring to the language means they are here to stay.