Skip to content

Bracket Axis Simulation for APL*PLUS II/386

Vector 11:2, page 150

implementations programming techniques

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

Introduction

A recent migration of mainframe APL2 applications to APL*PlusII/386 highlighted some language differences which had to be addressed.

One difference which proved particularly interesting concerned axis specifications. In APL2, some functions — mostly scalar dyadics — can take an axis spec which causes the data to be processed in ways which are unfamiliar to APL*PlusII/386 aficionados.

Because the target interpreter does not support these additional axis specifications, it was necessary to simulate them. This article describes how this was done, using APL*PlusII/386 Release 4 (or higher) in Evolution Level 2 mode. The examples in this article assume that ⎕IO is 1.

Time constraints enforced a selective approach. Basically, nothing was simulated unless it was used by the applications we were migrating. However, each simulation was programmed to handle all documented (APL2 Programming: Language Reference, IBM, SH20-9227-0) repertoire rather than just the application’s specific requirements.

Concept

To explore the concept, we’ll talk about the Add function.

When we add two numeric arrays together, they must be conformable. In APL*PlusII/386, ignoring the multi-rank singletons, all of which conform with each other, that means that they must be identically shaped or that one of them must be a scalar.

      2 + 3
5

      2 + 3 4
5 6

      2 3 + 4 5
6 8

      (⍉4 3⍴10 20 30) + 3 4⍴⍳12
11 12 13 14
25 26 27 28
39 40 41 42

In APL2, conformability also takes into account the fact that we can perform axial addition. So, in addition to the above forms, we can code

      20 30 40 +[1] 3 4⍴⍳12
21 22 23 24
35 36 37 38
49 50 51 52

In this example, the arguments are conformable because the shape of the left argument is the same as the shape of the right argument indexed by the axis specification. Arguments are conformable if (⍴L) = (⍴R)[X] or (⍴R)=(⍴L)[X].

We are not restricted to specifying a single axis, so we can code

      ⎕←A←2 3⍴1 2 3 7 8 9
1 2 3
7 8 9

      ⎕←W←2 2 3⍴⍳12
 1  2  3
 4  5  6
 7  8  9
10 11 12

      A+[2 3]W
 2  4  6
11 13 15
 8 10 12
17 19 21

Simulation

We couldn’t code the axis specifications verbatim in APL*PlusII/386, so the code had to be rearranged somewhat. In the event, it proved simplest to write a function for each axial use, combining the axis specification with one of the function arguments.

Here is how the APL2 expression in the previous example is recoded for the simulation

      A plus_with_axis (2 3) W
 2  4  6
11 13 15
 8 10 12
17 19 21

The processing technique adopted was to propagate the input array which was to be applied axially, so that it was conformable with the other input array.

Thus, returning to an earlier example

      20 30 40 +[1] 3 4⍴⍳12
21 22 23 24
35 36 37 38
49 50 51 52

the left argument was propagated to become

20 20 20 20
30 30 30 30
40 40 40 40

after which the addition could take place normally.

A glance at the APL2 Language Reference Manual (page 91) shows there to be twenty-one scalar dyadic functions. If their axial forms are to be simulated, their arguments are going to need this treatment. A utility function was therefore written to do this task. Each simulation function then called this conformer before doing the relevant APL*PlusII/386 operation on the results. Here is a listing of the conformer.

    ∇∆z←∆a conformer ∆w;∆A;∆E;∆Q;∆X;∆W;⎕IO
[1]   ⍝ Conform ⍺ and 2⊃⍵ for arithmetic on axes 1⊃⍵.
[2]   ⍝  (See "APL2 Programming: Language Reference"  pp95-96)
[3]
[4]   ⍝ ⍺  is target array (addend|subtrahend|multiplicand|dividend).
[5]   ⍝ ⍵  is 2-item vector:
[6]   ⍝      1) integer axis-number[s]
[7]   ⍝      2) modifier array (increment|decrement|multiplier|divisor).
[8]
[9]   ⍝ NB:  Conversely, 2⊃⍵ may be the target and ⍺ may be the modifier.
[10]
[11]  ⍝ ←  is one of:
[12]  ⍝      simple textvector errormsg OR
[13]  ⍝      2-item nested vector:
[14]  ⍝        1) conformed copy of ⍺
[15]  ⍝        2) conformed copy of ⍵.
[16]
[17]   ⎕IO←1
[18]   ∆A←∆a                             ⍝ Get target.
[19]   (∆X ∆W)←∆w                        ⍝ Get axis spec & modifier.
[20]   ∆X←(,∆X)[⍋,∆X]                    ⍝ Work with vector of axis nos.
[21]   ∆E←0                              ⍝ Start exchange-args flag.
[22]   →((⍴⍴∆A)>⍴⍴∆W)/AAA                ⍝ If 2⊃⍵ is target, and ⍺ is
[23]   (∆W ∆A)←∆A ∆W                     ⍝  modifier, swap them over
[24]   ∆E←1                              ⍝  and note the exchange.
[25]
[26]  AAA:
[27]   ∆z←'AXIS ERROR'
[28]   →((⍴∆X)≠⍴⍴∆W)/ZZZ                 ⍝ Axis for each axis of modifier?
[29]   →(0∊(∆X⍳∆X)=⍳⍴∆X)/ZZZ             ⍝ No duplicate axes?
[30]   →(0∊∆X∊⍳⍴⍴∆A)/ZZZ                 ⍝ All axes present in target?
[31]   ∆z←'LENGTH ERROR'                 ⍝ Check the modifier will map
[32]   →(~(⍴∆W)≡(⍴∆A)[∆X])/ZZZ           ⍝  to the stated target axes.
[33]
[34]   ∆Q←((⍳⍴⍴∆A)~∆X),∆X                ⍝ Put declared axes last.
[35]   ∆z←∆Q⍉((⍴∆A)[∆Q])⍴∆W              ⍝ Generate enough data.
[36]   ∆z←∆A ∆z                          ⍝ Make result on the assumption
[37]   →(~∆E)/ZZZ                        ⍝  we didn't swap the inputs.
[38]   ∆z←(2⊃∆z) ∆A                      ⍝ If we did, swap back.
[39]
[40]  ZZZ:
    ∇

Coding invocations

In APL*PlusII/386, it was necessary to recast axial invocations of the scalar dyadics to use the simulation functions. Thus, for example,

20 30 40 +[1]3 4⍴⍳12

became

20 30 40 plus_with_axis 1,⊂3 4⍴⍳12

The same form was used regardless of which argument was to be extended. Thus, to invert the above example,

(3 4 ⍴⍳12) +[1] 20 30 40

became

(3 4 ⍴⍳12) plus_with_axis 1,⊂20 30 40

Using this coding paradigm, manually recasting applications’ axial uses of the scalar dyadics became a fairly pedestrian affair.

Repertoire

Altogether, the migrated applications required two sets of simulation functions to be written. The first set comprised some scalar dyadic functions: and_with_axis, divide_with_axis, multiply_with_axis, or_with_axis, plus_with_axis and subtract_with_axis.

The second set comprised the monadic mixed function ravel_with_axis, and two dyadic mixed functions drop_with_axis and take_with_axis.

All the functions listed in the first set above were very similar. They all made use of the conformer. Here, as an example, is plus_with_axis again.

    ∇∆z←∆a plus_with_axis ∆w;⎕IO
[1]   ⍝  Return the sum of ⍺ and 2⊃⍵ on axes 1⊃⍵   ⍝ PUBLIC
[2]
[3]   ⍝ ⍺  is a numeric vector:  the addend.
[4]   ⍝ ⍵  is 2-item vector:
[5]   ⍝      1st item is integer scalar axis-number
[6]   ⍝      2nd item is the increment.
[7]   ⍝ ←  is the sum.
[8]
[9]    ⎕IO←1
[10]   ∆z←∆a conformer ∆w                ⍝ Conform ⍺ and ⍵ axially.
[11]   ⎕ERROR (1≥≡∆z)/∆z                 ⍝ Report any error.
[12]   ∆z←⊃+/∆z                          ⍝ Add.
    ∇

The three functions in the second set were not amenable to having their arguments conformed in this way. It was necessary to engineer a separate solution for each of them.

Ravel With Axis

APL2’s Ravel With Axis function is monadic, so the simulation function was written to take the axis specification as its left argument. The axis specification may be in one of four forms: empty, integer vector, integer scalar, and fractional scalar. The simulation function, after validating the arguments, checks which form applies, and then branches to the appropriate logic, which begins at line [25].

    ∇∆Z←∆A ravel_with_axis ∆W;∆Q
[1]   ⍝  Ravel ⍵ on axes ⍺.
[2]
[3]   ⍝ ⍺  is integer scalar or vector axis specification.
[4]   ⍝ ⍵  is variable to be ravelled.
[5]   ⍝ ←  is the result of the ravel.
[6]
[7]    →(1<⍴⍴∆A)/XXX                     ⍝ ⍺ must be rank-0 or rank-1.
[8]    →(∨/0>∆A)/XXX                     ⍝ ⍺ must all be positive.
[9]    →(0=⍴⍴∆A)/CCC                     ⍝ ⍺ ok if scalar.
[10]   →(0=⍴∆A)/CCC                      ⍝ ⍺ ok if empty vector.
[11]
[12]   →(~∧/0=1|∆A)/XXX                  ⍝ Bad ⍺ if fractions in vector.
[13]   →(~∧/∆A∊⍳⍴⍴∆W)/XXX                ⍝ Bad ⍺ if any axis nonexistent.
[14]   →(1=⍴∆A)/CCC                      ⍝ ⍺ ok if only one axis stated.
[15]
[16]   →(∨/∆A≠∆A[⍋∆A])/XXX               ⍝ Bad ⍺ if axes out of sequence.
[17]   →(∨/∆A≠∆A[1]+¯1+⍳⍴∆A)/XXX         ⍝ Bad ⍺ if axes not contiguous.
[18]
[19]  CCC:
[20]   →(0∊⍴∆A)/EEE                      ⍝ CASE: empty axis spec
[21]   →(1=⍴⍴∆A)/VVV                     ⍝ CASE: integer vector spec
[22]   →(0=1|∆A)/SSS                     ⍝ CASE: integer scalar spec
[23]   →FFF                              ⍝ CASE: fractional scalar spec
[24]
[25]  EEE: ∆Z←((⍴∆W),1)⍴∆W      ⋄ →ZZZ   ⍝ EMPTY SPEC: append an axis
[26]  FFF: ∆Z←((1,⍴∆W)[⍋∆A,⍳⍴⍴∆W])⍴∆W    ⍝ FRACTION SPEC: insert new axis
[27]   →ZZZ
[28]  SSS: ∆Z←∆W                ⋄ →ZZZ   ⍝ SCALAR INTEGER SPEC: ⍵ asis
[29]  VVV:                               ⍝ VECTOR INTEGER SPEC:
[30]   ∆Q←⍴∆W                            ⍝  combine indicated axes.
[31]   ∆Q←((¯1+∆A[1])↑∆Q),(×/∆Q[∆A]),(∆A[⍴∆A])↓∆Q
[32]   ∆Z←∆Q⍴∆W
[33]   →ZZZ
[34]
[35]  XXX: ⎕ERROR 'AXIS ⍺ ERROR'
[36]
[37]  ZZZ:
    ∇

Some examples of use follow.

An empty axis specification appends another axis

      ⎕←Q←(⍳0) ravel_with_axis 1 2 3
1
2
3
      ⍴Q
3 1

A fractional axis specification inserts another axis

      A←2 3⍴⍳6
      ⎕←Z←1.1 ravel_with_axis A
1 2 3
4 5 6
      ⍴Z
2 1 3

A vector of axis numbers causes the indicated axes to be combined as one axis

      M←2 3 4⍴⍳24
      ⎕←N←2 3 ravel_with_axis M
 1  2  3  4  5  6  7  8  9 10 11 12
13 14 15 16 17 18 19 20 21 22 23 24
      ⍴N
2 12

Drop With Axis

To simulate Drop With Axis, the drop specification is expanded to include a zero for each unreferenced axis and then an APL*PlusII/386 Drop (i.e. without axis) is done. This paradigm has proved serviceable for the migrated applications, and has produced correct results for the handful of examples in the manual (pages 197-198), although its ultimate correctness in the production of empty arrays is moot.

      ⎕←D←1 2 drop_with_axis (2 3)(3 4 5⍴⍳60)
 8  9 10
13 14 15
18 19 20
28 29 30
33 34 35
38 39 40
48 49 50
53 54 55
58 59 60
      ⍴D
3 3 3

Take With Axis

Take With Axis is quite a challenge, it being necessary to generate the correct prototype with which to extend each axis during over-taking. Again a simulation was produced which met the requirements of the migrated applications and ran the examples in the manual correctly.

![]DISPLAY T←2 3 4⍴'HOME',1 1 1 1,'FAIR',2 2 2 2,'MARK',3 3 3 3 and ]DISPLAY Z←¯5 6 take_with_axis (2 3) T, with ⍴Z of 2 5 6: the over-taken rows padded with 0 and blanks](take.png)

As this example demonstrates, when an over-take occurs on a given axis, the fill item is of the same type as the first item of each sub-array along that axis.

Conclusion

The absence of certain APL2 axial forms from the APL*PlusII/386 repertoire need be no barrier to a successful migration from mainframe APL2. What’s more, writing functions to simulate them is an entertaining exercise.

Of course, there were other APL2-isms besides these. Simulating ⎕TF was a mini-system in its own right. Simulating ⎕EA was rendered awkward by the applications’ inclusion of branch statements in ⎕EA arguments. Disclose was an interesting function which shrank as it improved, and then there were simulations of the APL2 Assembler functions such as CAN and DAN besides. But I think we’d better leave all these amusements for some future article.