Skip to content

Using Operators in APL2

Vector 2:3, page 118

language design programming techniques

Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 118

Nested arrays are the feature of APL2 which seems to generate most enthusiasm amongst its devotees, as witness for example the twenty or so pages devoted to that topic in Vector Vol 2 no.1. On the other hand operator extensions seems to receive relatively scant attention in spite of the fact that they provide at least as great an advance in linguistic power. There are two broad aspects of operator extension – first the provision of user-defined operators, and secondly the extension of existing operators. These two features, both separately and together increase by a huge factor the expressiveness of APL in describing programming ideas with succinctness and clarity.

In general if APL objects are the ‘nouns’ of the language and functions the ‘verbs’, then operators are the ‘adverbs’. They direct how to apply or combine functions in manners which are common across a range of functions. In APL1 the adverbial aspect of functions is achieved by ‘embroidering’ the function symbols with other symbols such as / and . . For example, +/V describes how to add the elements of the vector V, i.e. how to add ‘through’ V, in the sense of inserting the function “+” into all the available spaces (1 less than the number of elements in V) and evaluating the resulting expression. After defining reduction to describe the adverbial concept of ‘through’, it makes sense to talk about ‘multiplying through’, ‘dividing through’, and so on. It is logically perfectly possible to apply reduction to non-scalar dyadic functions, so that one can ‘reshape through’ and ‘join through’ vectors:

⍴/3 2 4  ←→  3⍴2⍴4  ←→  4 4 4

,/'NOT' 'WITH' 'STANDING'
        ←→  'NOTWITHSTANDING'

Such extensions provide in total a large amount of extra function in APL2. They are however conceptually straightforward, and I shall concentrate here on some of the subtler aspects of operator extension, which it will transpire are closely interwoven with enclosure and disclosure and thus with nested arrays.

User-Defined Operators

It is well known that in the case of non-commutative functions such as subtract and divide the effect of ‘through’ is to produce respectively the alternating sums and products. To many APL learners this result is counter-intuitive in that they feel that -/6 2 3 should mean 6-2-3, i.e. 1 rather than 7. This is indeed a reasonable variant of subtraction – call it subtraction ‘along’ a vector. The following lines describe the process for a general scalar dyadic function F, which could be either primitive or user-defined:

 Z←↑R
L0:→(0=⍴R←1↓R)/0
 Z←Z F↑R
 →L0

They define a new function called the ‘derived function’ with R as its right argument. We can generalise the process by specifying an axis G, and at the same time giving a name ‘along’ to what we now recognise as a new operator, with F and G as its two operands.

    ∇Z←(F ALONG G)R
[1]   Z←↑[G]R
[2]  L0:→(0∊⍴R←1↓[G]R)/0
[3]   Z←Z F 1↑[G]R
[4]   →L0
    ∇

Some applications are:

      (+ALONG 1)⍳4
10
      (-ALONG 1)⍳4
¯8
      (-ALONG 2)2 3⍴⍳6
¯4 ¯7
      ÷(÷ALONG 1)⍳4
24

(note that index-origin will be taken as 1 throughout.)

Now consider the + outer product of a vector V with itself. We define an operator directing how to add V in this context, i.e. by separately adding each of its elements with all the elements. The ‘each with all’ clause is an embroidery which could equally well be applied to a general scalar dyadic function F, and is defined by

    ∇Z←(F TABLE)R
[1]   Z←R∘.F R
    ∇

Thus

×TABLE ⍳12

is the ordinary school multiplication table.

Commuting function arguments provides another opportunity to embellish a range of functions with a common adverbial embroidery.

    ∇Z←L(F COM)R
[1]   Z←R F L
    ∇

2 *COM ⍳4  ←→  1 4 9 16

What is common to the operators ALONG, TABLE, and COM is that they are applicable across a range of functions – if the function variation is really relevant to only one function, there is no need to traffic with operators – user-defined functions can be used in the usual way.

Of the operands F and G, either or both may be functions or arrays, but for meaningful uses at least one should be a function. The derived function then takes arguments L and R. If G and R are both arrays as in the case of the function ALONG then brackets are necessary to show where G stops and R begins.

If the operator is monadic, then the single operand appears to the left, thus reversing the rule for arguments. This is because non-ambiguity of syntax demands that operators follow the reverse rule for functions, that is they are read from right to left and executed from left to right. Thus in the expression

(-COM ALONG 1)⍳4

the operator COM is executed before the operator ALONG. More specifically the derived function (-COM) is constructed and passed as an operand to ALONG to obtain the derived function (-COM) ALONG 1. Applying this derived function to the right argument, the successive execution steps are:

1(-COM)2 = 1
1(-COM)3 = 2
2(-COM)4 = 2

giving 2 as the final answer.

Further examples of situations in which user-defined operators suggest themselves naturally are given in three conference papers, my own on ‘Cultivating Trees’ in APL84, and those on ‘Operators for Program Control’ and ‘Operators for Recursion’ by E. Eusebi in APL85.

Extension Of APL1 Operators

The standard APL1 operators can be used to demonstrate further that the execution rule for operators is the opposite to that for functions. As in the previous article, I proceed by giving basic exercises, which you may like to work out by covering up the answers before reading them and the expository notes which follow. As before I shall use the function DR:

DR: (≡R),⍴R

to obtain the depth-followed-by-rho of an APL object.

What are:

(A)  1 2 +.×.- 3 4

(B)  1 2 +.(×.-) 3 4

It may be useful to enunciate the APL1 rule for an inner product F.G, viz. first perform G on an element by element basis for the last dimension of the left argument, and the first dimension of the right argument, then carry out F reduction. (Think of +.× and the ‘row-into-column’ rule for algebraic matrix multiplication). If the arguments of an inner product are scalars then the function F plays no part, i.e. F.G is equivalent to G.

In APL2 this rule has subtly changed – it is now :

For those arguments which are not scalars enclose elements along the last axis of the left argument and the first axis of the right argument, then evaluate the ‘F reduction each’ of the G outer product, enclosing every element.

Just as inner product evaluation begins with enclosure, so outer product begins with disclosure of the elements, followed by enclosure of the separate function evaluations.

In (A) the leftmost inner product is executed first, and so the final derived function is (+.×).-. The APL1 rules would suggest an answer

      (+.×)/¯2 ¯2
4

The APL2 rule for inner product first obtains the outer product

      (⊂1 2)∘.-⊂3 4
¯2 ¯2                        DR = 2

Notice that there is no initial enclosure because both elements are scalars. Evaluating the outer product however involves an initial disclosure, and a final enclosure which accounts for the depth of 2. ‘+.× reduction each’ then leads to the answer 4, and so the APL2 rule follows the old rule in this case – the subtle but important difference being that an enclosure and an ‘each’ have become involved, even although neither was present in the original expression.

In (B), following the APL1 rules, (×.-) should be equivalent to -, leading to a final result 1 2 +.- 3 4, namely -4. The APL2 rules, however, lead to an initial evaluation of the outer product

      (⊂1 2)∘.(×.-)⊂3 4
4                            DR = 0

The ‘+.× reduction each’ is then a null function and the final result is +4, which is not the same as the APL1 rules would have us expect.

Now introduce nesting. What are:

(C)  (⊂1 2) × ⊂3 4

(D)  (⊂1 2) +.× ⊂3 4

(E)  (⊂1 2) ∘.× ⊂3 4

(F)  +.×/(⊂1 2),⊂3 4

The answers are:

(C)  3 8            DR = 2

(D)  3 8            DR = 3

(E)  3 8            DR = 2

(F)  11             DR = 0

(C) illustrates the pervasiveness of the scalar dyadic functions, i.e. element by element multiplication takes place through the ‘barrier’ of enclosure. We shall assume henceforward that the concept of pervasiveness is well known and understood.

The answers (D) to (F) may seem surprising at first.

(i) Why is (D) depth 3 and not 2? APL1 intuition suggests that since both arguments are scalars the + should be inoperative and the result the same as (C). However the APL2 rule for outer product results in a singly enclosed vector as per example (E), while that for the inner product shows that each element of the final result is the enclosure of the + reduction - a consideration necessary to guarantee scalarness of elements in cases where the function is not scalar dyadic.

(ii) The non-equivalence of (D) and (F) violates the APL1 rule for reduction of a vector, that is obtain the F/ by placing the function F in all the available (possibly zero) spaces, and evaluating the result. This is because ensuring that the reduction operator always reduces rank requires a subtle variation for APL2, namely the ‘F each’ should be inserted between adjacent items, and the final evaluation enclosed. Since ‘each’ has no effect when no nesting is present this rule completely subsumes the APL1 rule.

Now add the ‘each’ operator explicitly into the mix. What are:

(G)  (⊂1 2) ר ⊂3 4

(H)  (⊂1 2) +.ר ⊂3 4

(I)  (⊂1 2) ∘.ר ⊂3 4

(J)  (⊂1 2) +.(ר) ⊂3 4

The answers are:

(G)  3 8            DR = 2

(H)  11             DR = 0

(I)  3 4            DR = 2
     6 8

(J)  3 8            DR = 3

The ‘each’ in (G) forces disclosure of the two arguments, with a final enclosure after multiplying.

(G), (H) and (I) demonstrate the ‘shell-penetration’ role of each. Comparison of (G) and (H) demonstrates how the implicit ‘each’ in the definition of inner product achieves an extra level of ‘shell-penetration’. Note the equivalence of (H) and (F).

Comparison of the results of (E) and (I) is also revealing. (E) is a straightforward multiplication table for the two scalar arguments. At first sight the ‘each’ in (I) ought not to make any difference since both arguments are scalars. This is however a circumstance in which the apparent simplicity of the name ‘each’ can be deceptive. The rule for ‘each’ must be strictly applied, that is, disclose both elements, apply the operand of ‘each’ (in this case outer product multiply), then enclose. Similar considerations apply to the pairs (H) and (D). Now comparing (H) and (J), in (H) the +.× outer product (i.e. the first step of the inner product) is in fact applied to simple objects following the implicit disclosures with ‘each’, whereas in (J) the initial ‘times each’ outer product of the two arguments leads to a nested vector, and the final enclosure implicit in the inner product gives rise to an ultimate depth of 3.

Consistency with Existing Concepts of Reduction and Inner Product

The discussion so far might suggest that the amendments to the rules for inner product and reduction impose an intolerable new layer of complexity on the APL2 user. They are however no more than necessary adjustments which ensure that what is intuitively obvious and desirable should happen in the presence of nestedness. This is illustrated by the remaining examples for which we define B as the nested array:

┌───┐   ┌───┐
│0 1│   │2 0│
└───┘   └───┘
┌───┐   ┌───┐
│1 0│   │0 2│
└───┘   └───┘

that is

B←2 2⍴(⊂0 1),(⊂2 0),(⊂1 0),⊂0 2

What are:

(K)  1 2 +.× B

(L)  (⊂1 2) +.× B

(M)  (⊂1 2) +.ר B

(N)  (⊂1 2) +.(ר) B

Answers and notes:

(K)  (2 1)(2 4)       DR = 3 2

that is

((1×⊂0 1)+2×⊂1 0)      ((1×⊂2 0)+2×⊂0 2)
(L)  (1 2)(2 4)       DR = 3 2

scalar left argument forces expansion to vector of length 2. Result is

(((⊂1 2)×⊂0 1)+(⊂1 2)×⊂1 0)
              (((⊂1 2)×⊂2 0)+(⊂1 2)×⊂0 2)

If the elements of B are taken to represent displacement vectors in 2D space (or forces, velocities, etc.) then (K) obtains V+2W where V and W are two elements belonging to the same column, and (L) gives V’+W’ where V’ and W’ represent V and W with x- and y- ‘stretch factors’ 1 and 2 respectively applied to each displacement.

(M)  2 2              DR = 1 2 2
     1 4

that is the +.× inner products of 1 2 with each of the 4 elements of B.

(N) – same as (L) on account of pervasiveness of multiplication.

The next two examples extend examples (A) and (B) and demonstrate the working of consecutive inner products.

(O)  (⊂1 2) +.+.× B

(P)  (⊂1 2) +.(+.×) B ?

Answers and notes:

(O)  3 6              DR = 1 2

(P)  1 2      2 4     DR = 4 2

In both cases the scalar left argument forces scalar expansion. In (O) the derived function is (+.+).×, and the elements of the result are

(⊂+.+/(0 1)(2 0))      (⊂+.+/(2 0)(0 4))

from which application of the reduction rule leads to the result.

In (P) the corresponding elements are

(⊂+/((⊂1 2)+.×⊂0 1),((⊂1 2)+.×⊂1 0))
     (⊂+/((⊂1 2)+.×⊂2 0),((⊂1 2)+.×⊂0 2))

The highest level enclosures arise from the last step of the inner product rule, and give rise to the depth of 4.

Summary

As with nested arrays the power of operators in APL2 is considerable and make the operator capabilities of APL1 look minute! At first sight the user may be understandably daunted by the sheer amount of variation afforded by the different possible ways of combining APL2 operators, quite apart from the effects of mixing these with operators of his or her own definition. Also the subtle variations in the inner product, outer product and reduction rules demand careful study in the first place, but will ultimately reveal a beautiful consistency with the simpler APL1 ideas. Some of the language extensions of APL2 provide a substantial intellectual challenge and require a systematic and disciplined approach to master them, these articles may hopefully have provided some pointers.