Skip to content

APL and J (4): Function Application and Axis

Vector 13:4, page 113

language design programming techniques

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

This series of articles entitled APL and J will try to explain the J way of thinking, by contrasting APL and J coding examples. The term APL will be used generically to refer to facilities found in most commercial implementations of APL.

Continuing the discussion of APL’s operators, we now look at the axis operator, and the whole question of how functions apply to arrays.

Housekeeping

As usual, we use the letters V, M, and A for vectors, matrices and arrays of any dimension, optionally followed by numbers to denote their shape. Thus V for any vector, V5 for a vector of length 5, M45 for a 4-by-5 matrix, A245 for a 2-by-4-by-5 array. Where actual values are required, V5 will be defined as ⍳5, M45 as 4 5⍴⍳20, for example:

      M45
 1  2  3  4  5
 6  7  8  9 10
11 12 13 14 15
16 17 18 19 20

APL terminology will be used, except where J terminology is required for clarity. Here, ⎕io←1 is assumed throughout.

Note that the behaviour of the axis operator is not identical in all APLs. This does not affect the gist of this article, but you may find that, with your copy of APL, the examples are not exactly as shown.

Overview

In this article, we cover what is perhaps the most important single difference between APL and J - the way functions apply to arrays. The experienced APLer may be surprised that there is anything to say at all, since it is not obvious where J could possibly differ from APL. Yet there are major differences that can be quite difficult to grasp. To explain what is going on, we will construct a model for APL, that can also be applied to J.

Function Application in APL

We start off with a description of function application in APL. We consider the monadic application of f to A and ask whether it is possible to split A up into subarrays in such a way that applying f in turn to each subarray, then assembling the results, would be the same as applying f to A. If so, we could consider f as being, in effect, defined on those subarrays. The following examples should make this clear.

Signum

Consider the signum function applied as below:

      ⎕← M← M23 - 3
¯2 ¯1 0
 1  2 3

      ×M
¯1 ¯1 0
 1  1 1

Clearly we could apply signum to each scalar element of M in turn, with the same effect. Thus ×M is equivalent to the following array of function applications:

(ׯ2) (ׯ1) (×0)
(×1)  (×2)  (×3)

Since signum can apply to each scalar in turn, we say that it applies to scalars, or equivalently, since scalars have rank 0, we say that it is a rank-0 function.

Note that this is not the usual meaning of the APL term scalar function, which means a function that not only applies to scalars, but also returns scalars. In fact, signum is such a function. However in this discussion, we are classifying functions by how they apply, and not by the results they return.

For example, we could have a function that applies to scalars, but which does not return scalar results. For such a function, we would need some way of determining how the overall result is assembled. There are essentially two such assembly schemes in use:

  1. Suppose sf is the shape of the array of function applications (for example, sf would be 2 3 in the signum example above), and that sr is the shape of the result of each function application, then we could assemble the overall result as an array of shape sf,sr. This scheme works fine as long as the result of each function application does indeed have a common shape, sr. If not, then the individual results would first have to be coerced into the same shape, typically by prefixing unit axes, and padding with fill elements.
  2. In the second scheme, the result of each function application is enclosed, and therefore is a scalar. Consequently, sr is empty and the overall shape of the result is sf.

Both these schemes are implemented in APL, but only the first in J. Also, in APL, the result shape in the first scheme is not always sf,sr, but can be some permutation of this.

The two schemes are easily interchangeable. The second scheme can be obtained from the first by using a function that does an explicit enclose of its result; while the first scheme can be obtained from the second by doing a disclose of the final result (plus a transpose where the APL result is a permutation of sf,sr).

Sum

Now let’s look at +/ and note that unlike signum, it does not apply to scalars. If we tried to apply it to scalars, as in +/ applied to M23 below, this would result in M23 unchanged:

(+/1) (+/2) (+/3)
(+/4) (+/5) (+/6)

We could say that +/ applied with rank 0 is the identity function.

On the other hand, it should be clear that +/ applies to vectors, and is therefore a rank-1 function. For example, we could apply it as:

      (+/1 2 3) (+/4 5 6)
6 15

Not only can we do this, but in fact in modern APLs, this is the definition of +/. Strictly speaking, the definition has an implied enclose after each function application (and so uses the second assembly scheme described above), but since the enclose of a scalar is the same scalar, the enclose has no effect:

      (⊂+/1 2 3) (⊂+/4 5 6)
6 15

In describing a function as being rank-n, we mean that n is the lowest rank at which it works correctly. On higher rank arrays, the function application can be considered as partitioning the array into subarrays of rank n, then applying the function to each subarray, and assembling the results. Thus both signum (a rank-0 function), and +/ (a rank-1 function) work fine on matrices, or arrays of higher rank.

Ravel

It should be clear that the usual result of ravel is changed if it is applied with a rank less than the rank of its argument. For example, suppose we tried to apply it with rank 0 to M23, and used the first assembly scheme. The result of each function application to a scalar would be a vector of length 1, and we would have:

      ⍴ (, with-rank-0) M23
2 3 1

If we used the second assembly scheme, then each function application would be enclosed, and the result would have shape 2 3. This is, in fact, precisely what ,¨ does:

      display ,¨M23
┌→────────────┐
↓ ┌→┐ ┌→┐ ┌→┐ │
│ │1│ │2│ │3│ │
│ └─┘ └─┘ └─┘ │
│ ┌→┐ ┌→┐ ┌→┐ │
│ │4│ │5│ │6│ │
│ └─┘ └─┘ └─┘ │
└∊────────────┘

(In general, f¨ means apply f as a rank-0 function, opening each scalar before applying the function, and using the second assembly scheme.)

We say that ravel is a whole-array function, or equivalently, that it is rank-infinity.

Axis

Now let’s look at what the axis operator gives us. If f is applied to an array A of rank n, and f is a rank-m function where 0 < m < n, then there will be more than one way of picking the rank-m subarrays of A. The axis operator lets you pick those subarrays. For example, if we apply the rank-1 function +/ to a matrix, then we might choose either the row-vectors or the column-vectors.

As another example, we could use axis to choose subarrays for ravel, as in:

      ,[2 3] A234
 1  2  3  4  5  6  7  8  9 10 11 12
13 14 15 16 17 18 19 20 21 22 23 24

This example means "apply ravel to axes 2 and 3 of A234", and is an example of the first assembly scheme described above (the individual results of ravel are not enclosed).

Also, as this example demonstrates, the axis operator not only allows you to pick the axes at which a function applies, but also, as a side effect, to specify the rank at which the function applies. Does this sound familiar - it should! The expression ,[2 3]A234 applies ravel with rank-2, to the subarrays along the last two axes.

We could also apply ravel with rank 1, as in ,[2]A234, but since ravel applied to a vector is the identity, then ,[2]A234 is the identity on A234 (this is an example where, when using the first assembly scheme, APL does not follow the result shape sf,sr). Hence ravel with rank 1 is also an identity function.

The notation for the axis operator also permits application with rank 0, as ,[⍬], though in practice this is not supported. If it were, we would expect that:

(⍴,[⍬]A)  ←→  (⍴A),1

APL Summary

At this stage, we have produced what seems to be a fairly simple and general model for function application in APL. We can classify functions according to the rank at which they are applied; we have two assembly schemes to put results together; and we can use the axis operator to specify axes where appropriate.

Unfortunately, this nice model does not always work! It has emerged from a consideration of how some functions actually behave, but does not describe how they were defined in the first place. There are simply too many exceptions for it to be consistent. We have seen one such already - the axis operator permits you to specify the rank at which a function is applied - except for rank 0 which has not been implemented! Here are some more exceptions:

Since ,[2 3] applies ravel to the last two axes, we might expect that ⊂[2 3] encloses the last two axes, which indeed it does:

      display ⊂[2 3] A234
┌→───────────────────────────┐
│ ┌→─────────┐ ┌→──────────┐ │
│ ↓1  2  3  4│ ↓13 14 15 16│ │
│ │5  6  7  8│ │17 18 19 20│ │
│ │9 10 11 12│ │21 22 23 24│ │
│ └──────────┘ └───────────┘ │
└∊───────────────────────────┘

However, try this with ⍉ or with ⌽:

      ⍉[2 3] A234
SYNTAX ERROR

      ⌽[2 3] A234
LENGTH ERROR

Not only do these not work, the error messages are wrong! The first shows a syntax error, but there is nothing wrong with the syntax, which is exactly that used earlier with ravel and enclose. The second shows a length error, but it is not possible for any monadic use of rotate to have a length error! When the APL interpreter itself gets error messages wrong, you know something strange is happening, and indeed the implementation of the axis operator is a patchwork of special cases.

Now let’s try using axis with a defined function. Define sum as:

    ∇ r←sum m
[1]   r←+/m
    ∇

and note that it does not work with axis:

      sum[1] M23
NONCE ERROR

Next, consider ⎕fi, which is clearly a rank-1 function, and try applying it to a matrix. As with +/, we might expect it to apply to each row in turn and then assemble the results together, but it fails:

      ⎕fi ⍕M23
RANK ERROR

We cannot complain that APL functions do not behave according to the model we have created, since no such model was used at outset. Yet there would obviously be a considerable benefit to programmers were a model of this type to be uniformly implemented.

Function Application in J

We start off by noting that the foregoing description of APL functions in terms of the rank at which they behave is identical in J. However, as mentioned earlier, J only follows the first assembly scheme (and therefore never does an implicit enclose of any result).

The big (and surprising) difference is that J has no axis operator! Whenever a function is applied to a set of axes in J, they are always taken from the end. Thus APL has first-axis functions, last-axis functions, and - using the axis operator - any-axis functions, whereas J has only last-axis functions. What J does have is a rank operator - which is just the side-effect noted earlier of APL’s axis operator.

For example, compare:

,[2 3] A234 APL
,"2 A234 J

The APL expression means apply ravel along axes 2 and 3 of A234. It happens that these are the last two axes, and also that ravel with axis uses the first assembly scheme.

The J expression means apply ravel with rank 2 to A234. Since axes in J are always the last axes, then it must be that ravel is applied to the last two axes; also J only uses the first assembly scheme. Therefore, it may be seen that the two expressions are identical.

Now consider the APL expression ,[1 2]A234. This ravels the first two axes of A234. There is no direct equivalent of this in J, since J has no way of specifying the first two axes. The only way to do it is to transpose the first two axes to the end, then ravel and transpose back:

   |: (,"2) 0 1 |: A234

At this stage, the picture we have is that functions apply to their arguments the same way in APL and J, but the operators that modify the behaviour are different: APL has axis, which includes rank, while J has only rank. With this picture, J seems so limited, you wonder how it works at all! But J has a few cards in hand.

Consistency

First note that while J’s model is simpler than APL’s, unlike APL, it is applied consistently to all functions. Lets go through the earlier examples where APL fails:

In J, you can use rank to apply a function to scalars. For example, ravel with rank 0 appends a unit trailing axis:

   $ ,"0 A234
2 3 4 1

In J, all functions work with rank:

   |: "2 A234                 transpose rank-2
 1  5  9
 2  6 10
 3  7 11
 4  8 12

13 17 21
14 18 22
15 19 23
16 20 24

   |."2 A234                  rotate rank-2
 9 10 11 12
 5  6  7  8
 1  2  3  4

21 22 23 24
17 18 19 20
13 14 15 16

You can use rank with explicit definitions:

   sum=: 3 : '+/y.'
   sum"1 M23
6 15

You can apply any rank-1 function to an array of higher rank:

   fi=: 0&".                  equivalent of ⎕fi
   10 * fi ": M23             applied to a character matrix
10 20 30
40 50 60

Dyadic Use

Unlike axis, J’s rank operator can be used to specify the left and right ranks of dyadic functions. In theory, axis could be used to do the same; for example, if a semicolon were used to delimit left and right axes, but this has not been implemented. For example:

   A=: 10 * B=: 1 2 3

   A +"0 1 B                  apply + with left rank 0, right rank 1
11 21 31
12 22 32
13 23 33
   A +"1 0 B                  apply + with left rank 1 right rank 0
11 12 13
21 22 23
31 32 33

Item Functions

On the face of it, J’s lack of an axis operator seems incredibly restrictive. While the example mentioned earlier of applying ravel to the first two axes of a 3D array may not seem very important, it is easy to think of important examples where axis seems essential - for example in determining whether +/ of a matrix applies along the rows or along the columns. It turns out, however, that the extra facilities provided by axis are typically not required in J. This is because of item functions, and prefix agreement.

J defines many functions, including +/, to apply to the items of an array, which are the subarrays consisting of all axes but the first, for example the rows of a matrix.

Thus in J, +/M23 is defined to be:

   1 2 3 + 4 5 6

Similarly, +/M234 is defined to be:

1  2  3  4        13 14 15 16
5  6  7  8    +   17 18 19 20
9 10 11 12        21 22 23 24

Note that this definition makes no use of axis, and indeed, +/ has rank infinity. There is no concept as in APL, of splitting up the argument into vectors, applying +/ to each vector, then re-assembling the results.

It turns out that +/ in J behaves like +⌿ in APL. Moreover, when you change the rank of +/ in J, you get the same effect as using the axis operator in APL. For example, the following are equivalent:

J APL
+/ A234 +/[1] A234
+/"2 A234 +/[2] A234
+/"1 A234 +/ A234

For example, +/"1 means apply +/ with rank 1, i.e. as a vector function. Since this is necessarily the last axis, it must be that +/"1 in J is the same as +/ in APL.

Prefix Agreement

J also minimizes the need for axis specification with an extension to APL’s scalar agreement known as prefix agreement. Arguments to a dyadic function agree if the frame of one is a prefix of the frame of another. In most cases, for ‘frame’ you can use ‘shape’ to the same effect (for further discussion, see the first article in this series, Vector Vol.13 No.1 pp96–101). Thus the following works in J:

   M34 + 10*V3
11 12 13 14
25 26 27 28
39 40 41 42

Summary

In both APL and J, we can classify functions by the rank at which they apply. Function application is essentially the same in APL and J, except only that APL sometimes assembles results after first enclosing them. A major difference is that in APL you can modify function application using the axis operator, whereas in J you use the rank operator, which is a subset of the axis operator.

Although it appears that the APL model has more functionality, it is not fully implemented. Moreover, for a proper comparison, you must supplement the J model by taking into account the use of item functions and prefix agreement. In practice, the J model works well.

Reference

Iverson, K.E. J Introduction and Dictionary, Iverson Software, 1996