6

I had a sneak peek at APL2 on the mainframe a number of years ago and remember being shown solutions to the problem of adding a vector to a matrix.

Given a←4 4 ⍴ ⍳16 and ⎕io←1

The old way of adding a vector to the rows was something like

a+(⍴a)⍴10 20 30 40

resulting in

11 22 33 44
15 26 37 48
19 30 41 52
23 34 45 56

and adding a vector to columns of a matrix was

a+(4 1⍴10 20 30 40)[;1 1 1 1]

or, if you prefer,

a+4/4 1⍴10 20 30 40

resulting in

11 12 13 14
25 26 27 28
39 40 41 42
53 54 55 56

Luckily, I was able to call up the guy who showed me APL2 that day (he's retired but still answers his phone) and ask about this second solution, who remembered right away what I was talking about.

The new APL2 way was much more concise, succinct, and consistent, those examples would be solved by a+[2] 10 20 30 40 and a+[1] 10 20 30 40. Cool. And it worked in Dyalog.

Fast forward a decade or more and I see there is this new thing called The Rank Operator. The first example can be solved by a(+⍤1) 10 20 30 40 (I am still trying to come to grips on the usage of parentheses, I think I actually regenerated some brain cells once I thought I understood a bit of this)

Be that as it may, there is no straightforward (at least to me) analogue to the second example a+[1] 10 20 30 40 using the rank operator. I can't say I understand it at all, but it seems to me that the rank operator "re-casts" its left argument by collapsing its dimensions while leaving the contents intact. Too many years of C++ and Java have influenced my way of thinking about things.

Is there a simple solution to a+[1] 10 20 30 40 using the rank operator? The only think I found so far is ⍉(⍉a)(+⍤1) 10 20 30 40 which misses the point and defeats the whole purpose.

Why would the rank operator be preferable to the axis notation? Which is "better"? (a loaded term, to be sure) At first glance, the axis notation was very easy for me, a guy with a shoe size IQ, to grasp. I couldn't say the same for the rank operator.

Apollo 42
  • 95
  • 6

3 Answers3

8

Is there a simple solution to a+[1] 10 20 30 40 using the rank operator?

Yes: a(+⍤1 0)10 20 30 40 Try it online!
 For dyadic application, the rank operator actually needs a two-element right operand (but accepts a singleton which it extends to two elements). a(f⍤A B)b means that a's rank-A subarrays should be paired with b's rank-B subarrays. So in your case, a(+⍤1)10 20 30 40 (which really means a(+⍤1 1)10 20 30 40) says that the rank-1 arrays of a (the rows), should be added to the rank-1 array(s) of 10 20 30 40 (the entire vector). Here, a(+⍤1 0)10 20 30 40, says that the rank-1 arrays of a (the rows), should be added to the rank-0 arrays of 10 20 30 40 (the scalars).

Why would the rank operator be preferable to the axis notation?

The rank operator allows you full control of what to grab from each argument, while bracket axis notation only allows you to extend the lower-ranking argument. The rank operator is an integrated part of the language, and can be used with any function, even user-defined ones, while the bracket axis notation can only be used with dyadic scalar functions, reductions, and a small subset of mixed functions.

Which is "better"?

Since the rank operator follows normal APL syntax for operators and is universally applicable, it reduces the amount of rules to remember. Furthermore, the rank operator also allows specifying relative ranks, by using negative numbers. So while ⍤1 means apply to subarrays of rank 1, ⍤¯1 means apply to subarrays of one lesser rank than the entire argument. In my opinion, this is more than enough to safely consider the rank operator "better" than bracket axis.

I am still trying to come to grips on the usage of parenthesis

I personally dislike parentheses, so I get where you are coming from. Luckily, you can always reduce the amount of parentheses as much as you like. The only reason for the parenthesis in a(+⍤1)10 20 30 40 is to separate the array operand 1 from the array argument 10 20 30 40. Any other way of separating them is acceptable too, and I usually use the identity function for this: a+⍤1⊢10 20 30 40 Try it online!
 However, you can also curry right operands to dyadic operators, yielding monadic operators, which read very nicely: horizontally←⍤1 0 ⋄ vertically←⍤1 1 Try it online!

The full rank operator documentation is available online

Community
  • 1
  • 1
Adám
  • 6,573
  • 20
  • 37
  • Thanks for your excellent comprehensive reply. I'm still short some brain cells, but it's getting better. I find the identity function construct a bit odd as it seems to me that the left argument to `⊢` in `a+⍤1⊢10 20 30 40` is being consumed twice, first in evaluating `1⊢10 20 30 40` (returning the right argument) then being part of the `a+⍤1` operator expression. I really like the currying thing even if it smacks just a bit of Haskell. Easy to understand and explain. – Apollo 42 Oct 28 '17 at 05:38
  • @Apollo42 Remember that operators bind before functions, so the `1` gets "caught" by `⍤` before it has a chance to before `⊢`'s left argument, and `⊢` is thus -called monadically. Btw, you're always welcome in [the Stack Exchange APL chat room](https://chat.stackexchange.com/rooms/52405/apl). – Adám Oct 28 '17 at 19:27
3

Over the years, one prevailing opinion is that square bracket usage, that is, indexing and the axis "operator" for functions, should be avoided. Some of the grounds for this is that the square bracket syntax is anomalous and inconsistent with the rest of the language, that the semantics of argument inside the square brackets differs amongst groups of functions, that it complicates parsing, and others. Google "apl axis operator anomalous" for lots of examples.

Indexing

Legacy indexing is the familiar x[i] form, alternatives include the "pick" and "squad" functions. Indexing array of higher dimensions uses the x[i;j;k;...;n] form where the ; separates individual dimension expressions. This expression can be elided to mean "all of that dimension". Then there is indexed assignment, where new values can be selectively inserted.

Axis Operator

First there were only provisions for axis specification for reduction +/[1]a, compression b/[1]a, expansion b\[1]a, and finally rotate 1⌽[2]x and reverse ⌽[2]x. Scan +\a came a just a little later, as did catenation x,[1] y and lamination x,[0.5] y. (Floating point "axis" - good grief) APL2 introduced more cases not unlike your example a +[2] b, plus enclose over an arbitrary coordinate ⊂[2].

The rank operator provides a unified way to handle coordinate specification.

Lobachevsky
  • 1,222
  • 9
  • 17
1

I believe that for adding a vector to a matrix you should no longer search for a simpler solution using the rank operator. I suppose that the rank operator was introduced to kind of achieve the behavior of primitive scalar functions also for defined functions. That is, if you had a defined function, say

∇Z←A ADD B
 Z←A + B
∇

then

a ADD[1] 10 20 30 40

would fail and force you into using the rank operator. The parentheses are needed for the rank operator because its syntax is somewhat dubious:

a +⍤1 10 20 30 40    ⍝ ???
a(+⍤1)10 20 30 40    ⍝ maybe this?
a(+⍤1 10) 20 30 40   ⍝ or maybe this?
a(+⍤1 10 20) 30 40   ⍝ or even this?

IBM was smart enough to standardize the rank operator but not to implement it (at least in my PC demo version of APL2).

  • What do you mean by *IBM was smart enough to standardize the rank operator but not to implement it*? AFAIK IBM never added any more functionality to APL2 after the initial implementation of the nested array system. Also, it isn't the rank operator which has dubious syntax; it follows the same syntax as all other operators. Rather, it is APL's stranding which makes adjacent arrays blur together. Without strand notation, `a +⍤1 (10,20,30,40)` would be unambiguous. – Adám Oct 28 '17 at 19:24
  • What I meant is that IBM wrote most of the ISO APL2 standard (which defines the rank operator) but they did not implement it themselves. Some people (including myself) consider it a "hack" if the function argument(s) of operators can be values. It is valid but looks odd. The problem in e.g. +⍤1 2 3 4 is that in APL2 the 1 binds stronger than the rest so it means (+⍤1) 2 3 4 which is difficult to comprehend if you are not familiar with the exact binding rules. And the 1 (which is called y in the ISO standard can have 1, 2 or 3 elements, but only the 1-element case can be achieved without (). – Jürgen Sauermann Oct 29 '17 at 21:23
  • OK, that *is* odd. Dyalog's rank operator is completely regular, and stranding [is always stronger](http://help.dyalog.com/16.0/Content/Language/Introduction/Binding%20Strength.htm) than operator binding. Allowing array operands makes operators like `⍣ ∘ ⍤ @ ⍠ ⌺` possible, and indeed very powerful. – Adám Oct 29 '17 at 22:45
  • As a matter of fact, IBM APL2, MicroAPL, and GNU APL use the same set of binding rules, while Dyalog APL uses a different one. For that reason, trying to remove parentheses as much as possible is not always a good idea, even if the code looks a little cleaner. In the context of ⍤ removing parentheses makes your code less portable and therefore less usable. – Jürgen Sauermann Oct 31 '17 at 11:40
  • Actually, Dyalog's syntax ports to other systems, just not vice versa. – Adám Oct 31 '17 at 11:46
  • Not sure what is meant by Dyalog's syntax, but if it refers do different binding rules in, say, IBM APL2 and Dyalog APL, then the incompatibility that these differences create are both ways due to the symmetry of ≠, i.e. since (A≠B)≡(B≠A) . – Jürgen Sauermann Oct 31 '17 at 15:11
  • Yes, the binding rules. And no, I don't think so, because Dyalog only requires parentheses that will still work in APL2, no? – Adám Oct 31 '17 at 15:33
  • Yes, but that is exactly what I meant by saying earlier that additional parentheses can remove ambiguity in certain cases and should not be removed. BTW Dyalog's tryapl.org gives me interesting results for ⍳⍤0 N3 with N3←1 2 3 (an example from the ISO standard). – Jürgen Sauermann Oct 31 '17 at 17:07
  • Yes, Dyalog requires parentheses in that case. (The result you get otherwise is meaningless.) – Adám Oct 31 '17 at 17:18