Rank in depth #

Rank, , is a dyadic operator which takes a function on its left, and on the right it takes a specification of which sub-arrays we want to apply that function to.

Simple usage of is specifying which rank subcells we want a function to apply to, and for dyadic usage, which subcells of the left argument should be paired up with which subcells of the right argument. Let’s say we have the vector 'ab' and the matrix 3 4⍴⍳12. We want to prepend ‘ab’ only the beginning of every row in the matrix:

(3 2'ab'),(3 4⍴⍳12)
ab 1  2  3  4
ab 5  6  7  8
ab 9 10 11 12

But here, we did so by reshaping 'ab' until it became big enough to cover all rows. How do we do this without reshaping 'ab', just using ?

'ab',13 4⍴⍳12
ab 1  2  3  4
ab 5  6  7  8
ab 9 10 11 12

Here we treat 'ab' as a cell and prepend it to every row of 3 4⍴⍳12. Let’s say instead we have a 3D array, and we want to put a single character from 'ab' on each row:

arr2 2 4⍴⍳16
(2 2'ab'),arr
 1  2  3  4
 5  6  7  8

 9 10 11 12
13 14 15 16
a  1  2  3  4
b  5  6  7  8

a  9 10 11 12
b 13 14 15 16

We can also do the same with rank, pairing up 'ab' with a matrix, not a row. When we concatenate a vector with a matrix, the vector becomes a new column:

'ab',22 2 4⍴⍳16
a  1  2  3  4
b  5  6  7  8

a  9 10 11 12
b 13 14 15 16

Now consider 'ABCD' and the following matrix:

2 4⍴⍳8
1 2 3 4
5 6 7 8

We want to produce the following,

2 4 2'A'1'B'2'C'3'D'4'A'5'B'6'C'7'D'8
A 1
B 2
C 3
D 4

A 5
B 6
C 7
D 8

We can see that each layer is each letter of the character vector paired up with each digit, each row in turn. So, for the first row of the matrix, we want:

'ABCD',01 2 3 4
A 1
B 2
C 3
D 4

We now want to apply this process for each of the rows. “For each row” is just ⍤1, and, yes, we can “stack” ranks:

'ABCD',012 4⍴⍳8 
A 1
B 2
C 3
D 4

A 5
B 6
C 7
D 8

Here is another example. Let’s say we’re constructing a lunch menu card. We have three “fillings” and four “containers”. We want to pair up all combinations of fillings and containers, thereby adding a trailing axis of length 2, so we get a rank 3 result:

'beef' 'fish' 'veggie'∘.{⍺⍵}'sandwich' 'patties' 'platter' 'wrap'
┌──────┬────────┐
│beef  │sandwich│
├──────┼────────┤
│beef  │patties │
├──────┼────────┤
│beef  │platter │
├──────┼────────┤
│beef  │wrap    │
└──────┴────────┘
┌──────┬────────┐
│fish  │sandwich│
├──────┼────────┤
│fish  │patties │
├──────┼────────┤
│fish  │platter │
├──────┼────────┤
│fish  │wrap    │
└──────┴────────┘
┌──────┬────────┐
│veggie│sandwich│
├──────┼────────┤
│veggie│patties │
├──────┼────────┤
│veggie│platter │
├──────┼────────┤
│veggie│wrap    │
└──────┴────────┘

Following the reasoning above, we can achieve the same thing with rank, using:

'beef' 'fish' 'veggie',00 1'sandwich' 'patties' 'platter' 'wrap' 
┌──────┬────────┐
│beef  │sandwich│
├──────┼────────┤
│beef  │patties │
├──────┼────────┤
│beef  │platter │
├──────┼────────┤
│beef  │wrap    │
└──────┴────────┘
┌──────┬────────┐
│fish  │sandwich│
├──────┼────────┤
│fish  │patties │
├──────┼────────┤
│fish  │platter │
├──────┼────────┤
│fish  │wrap    │
└──────┴────────┘
┌──────┬────────┐
│veggie│sandwich│
├──────┼────────┤
│veggie│patties │
├──────┼────────┤
│veggie│platter │
├──────┼────────┤
│veggie│wrap    │
└──────┴────────┘

We take each single item from the left argument, and whole right argument, which is ⍤0 1, and then each single left, with each single right, which is ⍤0 0 (or just ⍤0). The inner application is the single-single, so it needs to be closest to the function ,.

Also, remember that will not open your enclosures. It always operates on the elements of your arrays.

Time for another example. How can we swap the arguments to outer product just using rank (so no or )? In other words, go from this:

1 2 3∘.×1 2 3 4 5
1 2 3  4  5
2 4 6  8 10
3 6 9 12 15

to this:

1 2 3 4 5∘.×1 2 3
1  2  3
2  4  6
3  6  9
4  8 12
5 10 15

The first thing to note is that we can express the starting product as “each element to the left times the whole thing on the right”:

1 2 3×0 11 2 3 4 5
1 2 3  4  5
2 4 6  8 10
3 6 9 12 15

The reversed argument order then becomes “the whole thing on the left times each element to the right”, or simply the reversed rank:

1 2 3×1 01 2 3 4 5
1  2  3
2  4  6
3  6  9
4  8 12
5 10 15

A really useful function (let’s call it “Sane Indexing” or “Select”) is to select the major cells of the right argument as indexed by the left argument. For example, 2 3 1 2 Select 'abcdef' would give 'bcab'. Squad indexing, , only lets you choose a single major cell. Can we define Select in terms of with the help or rank? We need to pair each element from the left argument with the whole of the right argument, whatever rank it may be:

Select0 99
2 3 1 2 Select 'abcdef'
bcab

We could, in fact, have used any number greater than Dyalog’s max rank (15) to represent the full rank of the argument, but 99 has come to be used for this purpose. It is actually fairly common to want the target rank to be dependent on the argument rank. For that purpose, allows you to specify a negative number, which means that the target rank is that number subtracted from the argument rank. So f⍤¯1 ¯2 is the same as

{ f(¯1+≢⍴)(¯2+≢⍴)}

You can also mix-and-match positive and negative ranks.