8. Working on Data Shape#

In many programming languages, the programmer has to declare the dimensions of an array statically, and it is often possible to operate on the individual items using programmer-written loops.

In contrast to this, because APL processes arrays in their entirety, it is important to be able to manage the dimensions of an array dynamically. This is why this chapter presents a number of new tools that will help you perform these tasks.

We have already studied functions which create arrays with specific shapes:

Name

Syntax

Explanation

Section

Reshape

x⍴y

Creates a new array with dimensions x and the data from y.

Section 3.2.1

Catenate

x,y

Creates a new array by gluing x and y together.

Section 4.11

Ravel

,y

Creates a vector from any array.

Section 4.16

Compress

x/y

Selects parts of an array.

Section 4.12.1

Replicate

x/y

Generally replicates the items of an array.

Section 4.12.2

Indexing

x[y]

Creates a new array, often with modified dimensions.

Section 3.5

Index function

x⌷y

Creates a new array, often with modified dimensions.

Section 3.5.6

8.1. Take and Drop#

8.1.1. Take and Drop Applied to Vectors#

8.1.1.1. Starter#

There are two functions, take () and drop (), that can be used to extract or remove a number of elements from the beginning or end of a vector. The number is the left argument and its sign controls if the extraction/removal is done from the beginning or end of the vector.

  • Take extracts the vector’s head or tail; and

  • Drop removes the vector’s head or tail, and hence selects the remaining part.

Let us test these functions:

nums  56 66 19 37 44 20 18 23 68 70 82

With a positive left argument n, take extracts the first n items:

4nums
56 66 19 37

It works on any kind of data (numbers, text),

5'My name is Bond'
My na

including nested arrays:

2(6 2)(35 33 26 21)(42 73)(1 2 3)
┌───┬───────────┐ │6 2│35 33 26 21│ └───┴───────────┘

With a negative left argument n, it extracts the last |n items of the vector, in their normal order:

¯3nums
68 70 82
¯6'Mississippi'
ssippi

Sometimes, these two operations are referred to as taking the “head” of the vector (with a positive left argument) and taking the “tail” of the vector (with a negative left argument).

With a positive left argument n, drop removes the first n items and returns the tail:

4nums
44 20 18 23 68 70 82
5'My name is Bond'
me is Bond

With a negative left argument n, it removes the last |n items and returns the head:

¯6'Mississippi'
Missi

Remark

You will have noticed that 4↑nums and ¯7↓nums both gave the same result 56 66 19 37.

At first sight it would appear that there is no need for both of these functions, and that one or other of take and drop is redundant. There are, however, some differences that make it necessary to have both functions, as we will soon see.

8.1.1.2. Be Careful#

Do not confuse the two following expressions.

We can take the last three items of a vector:

¯3nums
68 70 82

Or we can take the first three items, and then change their sign:

-3nums
¯56 ¯66 ¯19

On another note, the result of take and drop applied to a vector remains a vector, even if it has only one item:

1nums
1
10nums
1

Although the results of the two expressions above would contain only one item, they are 1-item vectors, and not scalars.

8.1.1.3. Produce Empty Vectors#

Of course, if you take no items, or if you drop all the items, the result is an empty vector of the same type (numeric or character) as the original vector:

0nums
22nums
14'Empty'

Although all of the results above are empty vectors, we know that they are different if the original vectors have different types of data:

(22nums)(14'Empty')
0

Take a look at Section 3.7 for a reminder of how empty arrays work.

8.1.1.4. Take More Cash Than You Have#

The take function has a very special property: it allows you to take more items than there really are. If so, it pads the result with fill items; zeroes for a numeric vector, and blanks for a text vector:

cash  45 23 18 92

If we take too many items, zeroes are appended to the vector:

7cash
45 23 18 92 0 0 0

With negative left arguments, starting from the tail, the zeroes are placed before the existing items:

¯9cash
0 0 0 0 0 45 23 18 92

For character vectors, spaces are appended:

12'Invisible'
Invisible

However, you have to be careful because those do not show:

(12'Invisible'),'.'
Invisible .

Starting from the right, with a negative left argument, the blanks are appended to the left and are a bit easier to see:

¯12'Visible'
Visible

In fact, the concept of fill item is a bit more complex than this; it will be studied in detail in Section 10.9.

The concept of “taking more than you have” is sometimes referred to as overtaking. This is an application of take that cannot be performed using drop alone.

This property applies equally to empty vectors; they are filled with as many zeroes or blanks as specified. This means that the result will be different for empty numeric and empty character vectors:

]display 4
┌→──────┐ │0 0 0 0│ └~──────┘
]display 4''
┌→───┐ │ │ └────┘

8.1.2. Three Basic Applications#

8.1.2.1. Determine the Type of a Variable#

The property we have just seen can be used to determine whether an array is numeric or character, provided that it is simple and homogeneous (neither mixed nor nested). The method is simple: create an empty vector “filled” with the array (0⍴⍵), then take one item of it (1↑), and compare with 0. This will return 1 (true) for a numeric array, and 0 (false) for a character array. A little dfn will do that for us:

TypeOf  {0=10}
TypeOf nums
1
TypeOf 'Character vector'
0

This function wouldn’t work on a mixed or nested variable. Again, you will understand why when we talk about fill items in Section 10.9. We shall see in a later chapter that APL has a system function that does the job much better.

8.1.2.2. Change a Vector into a Matrix#

Sometimes you want a variable var to be a matrix, although you are not sure of its current rank.

If it is already a matrix, you want to leave it unchanged, and if it is a vector, you want to change it into a one-row matrix. The following function should help:

HorMat  {(¯21,⍴)}

Notice that the shape of the following matrix remains unchanged:

HorMat 3 5⍴⍳5
3 5

But the vector is “promoted” into a matrix:

HorMat 'This is a vector'
1 16

Let’s break down how HorMat works:

  1. we first append 1 to the shape of the argument, which gives 1 3 5 for the matrix and 1 16 for the vector;

  2. next, we keep only the last two items, which gives 3 5 for the matrix and 1 16 for the vector;

  3. the final reshape returns an unchanged matrix, or transforms a vector intro a matrix.

We can change vectors into 1-column matrices with a very similar function, which also leaves matrices unchanged:

VerMat  {(2(),1)}
VerMat 'Vertically?'
V e r t i c a l l y ?

8.1.2.3. Calculate Growth Rates#

Let us imagine a business with a turnover which has grown over 12 years. The variable tome is TurnOver in Millions of Euros:

tome  56 59 67 64 60 61 68 73 78 75 81 84

We want to calculate the difference between each year and the next; how do we achieve this?

1tome
59 67 64 60 61 68 73 78 75 81 84
¯1tome
56 59 67 64 60 61 68 73 78 75 81

All that remains is to subtract the results of these expressions one from the other, item by item:

(1tome)-(¯1tome)
3 8 ¯3 ¯4 1 7 5 5 ¯3 6 3

If instead of subtraction we used division, we would calculate (with some adjustments) the rates of growth instead of the differences. Let us upt that in a small dfn, and apply it:

Growth  {100×((1)÷(¯1))-1}
2Growth tome
5.36 13.56 ¯4.48 ¯6.25 1.67 11.48 7.35 6.85 ¯3.85 8.00 3.70

8.1.3. Take and Drop Applied to Arrays#

The functions take and drop can be applied to any array so long as the left argument contains a number of items that is equal to or smaller than the number of dimensions (the rank) of the array.

In the expressions ns↑array and ns↓array, ≢ns must be less than or equal to ≢⍴array.

For the examples that follow, we shall work on the following matrix:

mat  3 413 52 33 81 42 62 70 47 51 73 28 19

mat is an integer matrix but take and drop work the same for character arrays, and even for nested and/or mixed arrays.

8.1.3.1. Maximal-Length Left Argument#

We start by exploring what happens when the left argument has the maximal length it can have, that is, when (≢ns)=≢⍴array in the expression ns↑array or ns↓array.

If we want to take the 2 top rows and the 3 left columns of mat, we write

2 3mat
13 52 33 42 62 70

The elements of the left argument are paired up with the axes of the right argument array, and that is why 2 3↑mat takes 2 rows and then 3 columns, because the first axis specifies the number of rows and the second one the number of columns.

In order to drop the top row and 2 columns from the right, we write

1 ¯2mat
42 62 51 73

If we take 5 rows starting from the bottom and 3 columns starting from the left, we get

¯5 3mat
0 0 0 0 0 0 13 52 33 42 62 70 51 73 28

The two extra rows are added on the top of mat because we asked for 5 rows starting from the bottom.

Dropping 1 row starting from the bottom and 3 columns starting from the left, we get

¯1 3mat
81 47

In the example above, we have dropped 3 out of the 4 columns available, so there is only one left, but it is still a 1-column matrix; it has not been changed into a vector. This is similar to how 1↑vector in a vector returns a 1-element vector, not a scalar. In short, the functions take and drop return a result with the same rank as the right argument, regardless of the number of elements in the result.

As for vectors, it is often possible to use take or drop interchangeably to obtain the same result:

2 ¯3mat
52 33 81 62 70 47
¯1 1mat
52 33 81 62 70 47

8.1.3.2. Short Left Argument#

If one or more of the trailing dimensions of the array are to remain unchanged, then one need only specify the parameters for the leading dimensions.

For example, suppose that we want to extract the first 2 rows of mat. We know its shape is 3 4, so we can easily write

2 4mat
13 52 33 81 42 62 70 47

But if we didn’t know its shape in advance, we would have to resort to something like

(2,¯1↑⍴mat)mat
13 52 33 81 42 62 70 47

which is a bit cumbersome and, as it turns out, far from optimal.

In a matrix, the first axis denotes the rows, so we can pick the first 2 rows by just specifying the 2 in the left argument to take:

2mat
13 52 33 81 42 62 70 47

This works for arrays of arbitrarily high rank. For example, recall the 3D array prod, that contained monthly (12) production values of 2 assembly lines, throughout the course of 5 years:

⎕RL  73
 prod  ?5 2 1250
9 16 42 50 2 3 11 45 17 37 29 36 34 19 32 12 37 15 46 24 49 28 36 9 29 5 45 23 27 4 23 16 39 22 22 2 46 20 47 17 18 25 13 38 42 38 45 28 37 19 5 10 30 49 16 18 46 47 47 3 39 23 41 20 6 40 21 22 40 49 20 1 13 36 12 40 12 15 24 20 21 12 19 25 8 48 22 37 33 1 39 30 50 50 8 35 38 31 24 40 23 7 20 34 35 6 19 27 41 1 14 50 17 42 6 35 12 48 30 29

We can get the production values for the first 2 years with:

2prod
9 16 42 50 2 3 11 45 17 37 29 36 34 19 32 12 37 15 46 24 49 28 36 9 29 5 45 23 27 4 23 16 39 22 22 2 46 20 47 17 18 25 13 38 42 38 45 28

But we can also get the production values for the first 4 years and only for the first assembly line:

4 1prod
9 16 42 50 2 3 11 45 17 37 29 36 29 5 45 23 27 4 23 16 39 22 22 2 37 19 5 10 30 49 16 18 46 47 47 3 13 36 12 40 12 15 24 20 21 12 19 25

Naturally, drop can also be used with a left argument whose length is smaller than the rank of the argument. For example, we can drop the first row of a matrix:

1mat
42 62 70 47 51 73 28 19

Or we can drop the production values of the last year and of the first assembly line:

¯1 1prod
34 19 32 12 37 15 46 24 49 28 36 9 46 20 47 17 18 25 13 38 42 38 45 28 39 23 41 20 6 40 21 22 40 49 20 1 8 48 22 37 33 1 39 30 50 50 8 35

In Section 8.7 you can find a rule to compute the shape of the final result of a take (or drop) operation in terms of the left argument and the shape of the right argument.

8.1.3.3. Short Left Argument with Axis Specification#

When you provide a left argument that has less elements than the number of axes on the right argument, the left argument defaults to referring to the leading axes of the right argument. However, we can use take and drop with axis [] to specify other axes.

For example, the first 2 rows of mat were obtained with

2mat
13 52 33 81 42 62 70 47

Using axis, we can just as easily get the first 2 columns of mat:

2[2]mat
13 52 42 62 51 73

This also extends to higher rank. If we want the information regarding the first 3 years, and only the final 6 months of the year (but for all assembly lines), we can write:

3 ¯6[1 3]prod
11 45 17 37 29 36 46 24 49 28 36 9 23 16 39 22 22 2 13 38 42 38 45 28 16 18 46 47 47 3 21 22 40 49 20 1

8.2. Laminate#

We have previously used catenate to glue one array to another; let us now look at a new method.

We shall work with the following two character matrices:

 names  3 4'AlexAnnaMark'
Alex Anna Mark
 surnames  3 4'GainNairTyte'
Gain Nair Tyte

The catenation of those two matrices will not change the rank of the arrays, and thus give another matrix, as we saw in Section 4.11:

names,surnames
AlexGain AnnaNair MarkTyte
namessurnames
Alex Anna Mark Gain Nair Tyte

If both matrices have exactly the same shape, it is possible to join them together along a new dimension to make a 3D array. Because this operation produces a result of higher rank than its arguments, it is called laminate rather than catenate.

The symbol representing catenate and laminate is the same (,), but when the comma is used as laminate it is always used with a fractional axis.

The two arrays we intend to laminate have the same shape: 3 4. Because we are going to laminate 2 arrays, the new dimension will have a length of 2, and the shape of the result will be some combination of 3, 4, and 2. Let us examine all the possibilities:

  • 2 3 4   – the new dimension is inserted before the first dimension;

  •   3 2 4   – the new dimension is inserted between the first and second dimensions;

  •   3 4 2 – the new dimension is inserted after the second dimension.

To obtain these three different results, we shall use laminate with a fractional axis to specify where the new dimension is to be inserted:

  • names,[0.5]surnames will produce a result of shape 2 3 4  ;

  • names,[1.5]surnames will produce a result of shape   3 2 4  ; and

  • names,[2.5]surnames will produce a result of shape   3 4 2.

Beware that we could have equally used .

Here are the three cases:

names,[0.5]surnames
Alex Anna Mark Gain Nair Tyte
names,[1.5]surnames
Alex Gain Anna Nair Mark Tyte
names,[2.5]surnames
AG la ei xn AN na ni ar MT ay rt ke

In fact, the value of the axis specifier just identifies the position of the new dimension relative to the values 1 and 2, so it could be any other fractional value between 0 and 1, or 1 and 2, or 2 and 3, respectively.

Hence, the three matches that we see below:

(names,[0.295]surnames)names,[0.5]surnames
1
(names,[1.643]surnames)names,[1.5]surnames
1
(names,[2.107]surnames)names,[2.5]surnames
1

Of course, it would be somewhat obtuse to use such axis specifications, and programmers conventionally use “n.5” values, like the ones in our examples.

8.2.1. Applications to Vectors and Scalars#

Now that we understand the reason for the fractional axis, which is perhaps initially somewhat surprising, we can apply laminate to all kind of arrays.

8.2.1.1. Laminate Applied to Vectors#

Let us use both character and numeric vectors:

t1  'tomatoes'
t2  'potatoes'
n1  14 62 32 88
n2  10×⍳4

If we catenate them, we still obtain vectors:

t1,t2
tomatoespotatoes
n1,n2
14 62 32 88 10 20 30 40

But if we instead laminate them, we obtain matrices with either 2 rows or 2 columns:

t1,[0.5]t2
tomatoes potatoes
t1,[1.5]t2
tp oo mt aa tt oo ee ss
n1,[0.5]n2
14 62 32 88 10 20 30 40
n1,[1.5]n2
14 10 62 20 32 30 88 40

Of course, since we are working with 1-dimensional arrays, we cannot specify an axis equal to or greater than 2.

There is also no reason for why you wouldn’t be able to laminate a character vector with a numeric vector, for example:

t1,[0.5]n1,n2
t o m a t o e s 14 62 32 88 10 20 30 40

8.2.1.2. Laminate Scalars with Vectors#

Scalars can be laminated with any array: they are repeated as many times as necessary to match the length of the new dimension:

n1,[0.5]0
14 62 32 88 0 0 0 0
1,[1.5]n1
1 14 1 62 1 32 1 88

This can be used, for example, to underline a title:

title  'Laminate is good for you'

Without laminate, we must create a matrix with 2 rows, and as many columns as the length of title, filled with title itself, followed by as many dashes as the length of title: boring!

(2,≢title)title,(title)'-'
Laminate is good for you ------------------------

Now, with laminate, we just have to laminate a single dash; it will be repeated as many times as necessary:

title,[0.5]'-'
Laminate is good for you ------------------------

8.2.2. Applications#

8.2.2.1. Interlace Matrices#

Do you remember that, in the chapter about user-defined functions, we wrote a function to interlace two matrices? It is no longer relevant; we can solve the problem more simply using laminate.

Can you see how? Give it some thought, if you will.

Take a look at the result of names,[2.5]surnames above. You will see that the names are on the left and the surnames are on the right. If we reshape that result with appropriate dimensions, we shall obtain names and surnames interlaced 😊 :

(1 2×⍴names)names,[2.5]surnames
AGlaeixn ANnaniar MTayrtke

The result isn’t easy to read, but the expression works!

Hence, our new implementation of Interlace could be

Interlace  {(1 2×⍴),[2.5]}

We can apply the same technique to matrices of forecasts and actuals, provided we define them first:

⎕RL  73
forecast  10×?4 655
⎕RL  73
actual  forecast + ¯10+?4 620
forecast Interlace actual
90 89 160 166 420 420 500 508 20 12 30 23 110 111 450 453 170 177 370 365 290 284 360 352 340 349 190 192 320 329 120 115 510 515 370 374 150 160 460 467 240 234 520 519 490 485 280 283

8.2.2.2. Show Vectors#

Sometimes you will have two vectors that are related, and you would like to display them side by side. You might have a character vector and you would like to display it side by side with a Boolean vector indicating the positions of vowels in the character vector, so that you can verify visually if the result is correct:

text  'National Aeronautics and Space Administration'
vowels  0 1 0 1 1 0 1 0 0 0 1 0 1 0 1 1 0 1 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0 0 1 0 1 1 0
text,[0.5]vowels
N a t i o n a l A e r o n a u t i c s a n d S p a c e A d m i n i s t r a t i o n 0 1 0 1 1 0 1 0 0 0 1 0 1 0 1 1 0 1 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 1 0 1 0 0 0 1 0 1 1 0

By looking at the above, you would be able to see more easily that your result missed the capital vowels, and you would then proceed to fixing your function.

This was a simple example of where laminating two vectors might prove useful, but this ad-hoc technique might also be useful in slightly more complex scenarios. Suppose that we have four vectors containing information about certain products: their price, their current discount, their quantity in stock, and their availability (physical store, online, or both):

 price  2 4 15 8 23
 discount  0 0 25 25 35
 stock  103 98 50 23 64
 availability  'SSBOB'
2 4 15 8 23
0 0 25 25 35
103 98 50 23 64
SSBOB

The output shown above is not ideal, because each individual vector is displayed using its own natural format, and it is extremely difficult to connect the four related pieces of information to a specific product. We can have these values displayed much better if we create a matrix.

To produce a matrix we will need to laminate two of the vectors and catenate the others (in rows or in columns). The results are much easier to read:

pricediscountstock[0.5]availability
2 4 15 8 23 0 0 25 25 35 103 98 50 23 64 S S B O B
price,discount,stock,[1.5]availability
2 0 103 S 4 0 98 S 15 25 50 B 8 25 23 O 23 35 64 B

In these examples, there is only one laminate, followed by as many catenates as needed.

8.3. Expand#

8.3.1. Basic Use#

You remember that simple compress uses a Boolean vector of 1s and 0s as a mask to include or exclude specific items of an array.

Simple expand (specified by the \ symbol) also uses a Boolean vector of 1s and 0s, but the 0s insert new items into the array. It is used as follows: r pattern\argument.

In this form, the Boolean vector left argument contains a 1 for each item of the right argument, and a 0 for each item to insert. For example:

1 1 0 1 0 0 1 1 1\11 28 32 40 57 69
11 28 0 32 0 0 40 57 69
1 1 0 1 0 0 1 1 1\'Africa'
Af r ica

If the right argument is numeric, expand inserts zeroes, and if it is a character vector, expand inserts blanks as fill items. For mixed or nested arrays, the concept of fill item is more complex and, as mentioned before, will be explained in Section 10.9.

8.3.2. Extended Definition#

We can extend the behaviour of extend to handle cases where the left argument is not a simple Boolean vector, but contains integers other than just 0s and 1s:

  • the amount of positive numbers in the left argument should match the length of the right argument;

  • for each positive item in the left argument, the corresponding item in the right argument is replicated as many times as is specified by that value;

  • each negative item in the left argument inserts an equivalent number of fill items in the same position; and

  • zeroes in the left argument mean the same as ¯1, and they each insert one fill item.

In the description above, we assumed that the right argument is a vector. We will cover the fully generic case in a bit.

Because it is an extension of it, this new definition is fully compatible with the Boolean case we described before.

Here is an example:

1 1 0 3 ¯2 1 1 1\11 28 32 40 57 69
11 28 0 32 32 32 0 0 40 57 69

The first two items remain unchanged. Then a zero inserts a zero in the result. The next value is repeated 3 times, and the value ¯2 inserts 2 zeroes. The last 3 items are unchanged.

The same thing can be done using a character vector:

1 1 0 3 ¯2 1 1 1\'expand'
ex ppp and

Because 0 and ¯1 have the same effect when used in the left argument of expand, we can obtain the same result with a different pattern:

1 1 ¯1 3 ¯2 1 1 1\'expand'
ex ppp and

Naturally, the function can work on any shape of array, in which case \ acts on the last axis of the right argument, by default.

For example, if we take our chemistry matrix:

 chemistry  3 5'H2SO4CaCO3Fe2O3'
H2SO4 CaCO3 Fe2O3

We can insert one extra column by using expand with a Boolean left argument:

1 1 0 1 1 1\chemistry
H2 SO4 Ca CO3 Fe 2O3

We can further specify the axis along which we want to act, to change the default:

1 0 1 0 1\[1]chemistry
H2SO4 CaCO3 Fe2O3

The axis specified can be the same as the default, making it redundant, but otherwise completely correct:

1 ¯3 1 1 1 0 3\[2]chemistry
H 2SO 444 C aCO 333 F e2O 333

With arrays of rank higher than 1, the description provided above remains the same, except that extend no longer operates on single items of the right argument, but on its sub-arrays along the given axis (the last one, if left unspecified) instead.

Expand can also be used on scalars; they are repeated as many times as necessary to fit the number of positive values in the left argument:

0 0 1 1 0 0 1 1\'A'
AA AA

The above is equivalent to

0 0 1 1 0 0 1 1\'AAAA'
AA AA
0 1 3 ¯2 1 1\71
0 71 71 71 71 0 0 71 71

8.3.3. Expand Along First Axis#

Like it was mentioned before, expand works on the last dimension of an array. To work on the first dimension, one can use the function (APL+.):

1 1 ¯2 1chemistry
H2SO4 CaCO3 Fe2O3

If one places an axis indication after the symbol \ or , the operation is processed according to the axis operator, whichever of the two symbols is used. For example:

  • vec⍀[3]prod and vec\[3]prod would be equivalent to vec\prod; and

  • vec\[1]forecast and vec⍀[1]forecast would be equivalent to vec⍀forecast.

8.4. Reverse and Transpose#

APL is also well endowed with functions which pivot data about an axis, and the axis is suggested by the shape of the symbol used. The functions apply to both numeric and character data. In the examples we are going to use a character matrix called towns:

 towns  6 10'Canberra  Paris     WashingtonMoscow    Martigues Mexico    '
Canberra Paris Washington Moscow Martigues Mexico

The symbols (APL+Shift+5) and (APL+Shift+7) are used for two variants of the same function, which is called reverse.

The function is called transpose and its symbol is typed with APL+Shift+6.

Here are their effects:

(towns) (towns) (towns) (towns)
┌──────────┬──────────┬──────────┬──────┐ │Canberra │ arrebnaC│Mexico │CPWMMM│ │Paris │ siraP│Martigues │aaaoae│ │Washington│notgnihsaW│Moscow │nrssrx│ │Moscow │ wocsoM│Washington│bihcti│ │Martigues │ seugitraM│Paris │esioic│ │Mexico │ ocixeM│Canberra │r nwgo│ │ │ │ │r g u │ │ │ │ │a t e │ │ │ │ │ o s │ │ │ │ │ n │ └──────────┴──────────┴──────────┴──────┘

The symbols used ( ) are self-describing, no effort is required to remember any of them because the position of the bar visually indicates which kind of transformation they stand for.

If you insert an axis specification after the symbols or , the operation is processed according to the axis operator, whichever of the two symbols is used. So, for example:

  • ⌽[1]matrix and ⊖[1]matrix are both equivalent to ⊖matrix; and

  • ⌽[2]matrix and ⊖[2]matrix are both equivalent to ⌽matrix.

8.4.1. Caveats to be Aware Of#

  • Transpose has no effect on a vector, because it has only one axis:

'I shall not move'
I shall not move

On a similar note,

  • The distracted APLer might expect to do nothing on vectors, but that is not what happens.

Visually, hints at the fact that it “flips” its argument upside down, and a vector can’t be flipped upside down because it is one-dimensional. However, is called reverse first, which means it always reverses the argument along its first dimension, even if that dimension is the only dimension of the argument:

'I shall not move...!?'
?!...evom ton llahs I

This confusion might arise from the visual similarity of the character vector above and

1 21'I shall not move...!?'
I shall not move...!?

The latter is a 1-row matrix, which is why it is left unchanged by :

1 21'I shall not move...!?'
I shall not move...!?
  • Transpose cannot be modified by an axis specifier, because it always operates on all of the dimensions of its argument.

  • Transpose can be applied to arrays of any rank; let us try it with a 3D character array:

 big  names,[0.5]surnames
Alex Anna Mark Gain Nair Tyte
big
AG AN MT la na ay ei ni rt xn ar ke
big
2 3 4
⍴⍉big
4 3 2

You can see that ⍴⍉big is equal to ⌽⍴big, and the explanation of why this is the case will follow shortly.

All three primitives we just discussed also have (related) dyadic meanings, and those are introduced in two following sections.

8.5. Rotate#

The symbols and , when used dyadically, shift the items of the right argument in a circular manner.

The dyadic functions are called rotate.

8.5.1. Rotate Vectors#

Much like in the monadic case, the dyadic usage of and is identical when applied to vectors (we shall use in our examples because of the visual clue that the symbol gives with respect to the operation it performs).

Dyadic (and ) expects an integer as the left argument, when the right argument is a vector, like so: r n⌽vector.

  • when n is positive, the first n items of vector are moved to the end. In other words, the vector is rotated to the left; and

  • when n is negative, the last n items of vector are moved to the beginning. In other words, the vector is rotated to the right:

7'What did they do to my song?'
d they do to my song?What di

In the example above, the first 7 items of the vector ('What di') have been moved to the back, whereas if we use a negative argument:

¯7'What did they do to my song?'
y song?What did they do to m

the last 7 items of the vector ('y song?') are moved to the front of the vector.

Rotate can of course be applied to numeric vectors as well:

nums
56 66 19 37 44 20 18 23 68 70 82
3nums
37 44 20 18 23 68 70 82 56 66 19

Do not confuse the following two expressions:

¯3nums
68 70 82 56 66 19 37 44 20 18 23
-3nums
¯37 ¯44 ¯20 ¯18 ¯23 ¯68 ¯70 ¯82 ¯56 ¯66 ¯19

The first one moves 3 items to the beginning, whilst the second expression moves the first 3 items to the end, and then changes the sign of the result (we saw something very similar when we talked about take). It is all about being careful with the normal and high minus symbols!

8.5.2. Rotate Higher-Rank Arrays#

When applied to a matrix or higher-order array, works on the last dimension, while works on the first dimension. This default behaviour can be overriden by an axis specification. To obtain a rotation along any other dimension, the axis specification is mandatory.

Rotate can be applied to any array, but we shall only demonstrate its application to matrices.

8.5.2.1. Uniform Rotation#

In its simplest form, rotate applies the same rotation to all the rows or columns of a matrix; let us see the result produced on a character matrix:

 monMat  6 8'January FebruaryMarch   April   May     June    '
January February March April May June
2monMat
nuary Ja bruaryFe rch Ma ril Ap y Ma ne Ju
2monMat
March April May June January February
¯2monMat
May June January February March April

8.5.2.2. Multiple Rotations#

It is possible to apply a different rotation to each of the rows or to each of the columns.

In this case, the rotation is no longer indicated by a single value, but by a vector which specifies the amount by which each row or column will be moved.

chemistry
H2SO4 CaCO3 Fe2O3
¯1 0 2chemistry
4H2SO CaCO3 2O3Fe

Notice how ¯1 was used to rotate the first row, 0 was used to rotate the second row, and 2 was used to rotate the third row.

Using , we can rotate columns:

monMat
January February March April May June
1 0 2 ¯2 0 0 2 2monMat
Far ar Mereua Aayuh Mpnrl Janc y Jubi ry

8.5.2.3. Application#

Rotate can provide very simple solutions to many tasks. For example, let us count how many blanks appear at the end of each row of monMat:

+/' '=monMat
1 0 3 3 5 4

We can then use these values to move the blanks to the beginning of each row, thereby right-aligning the matrix:

(-+/' '=monMat)monMat
January February March April May June

8.6. Dyadic Transpose#

Dyadic transpose is interesting only for arrays of rank higher than 2. It rotates an array as if to show it from different angles.

Remember our variable called prod:

prod
9 16 42 50 2 3 11 45 17 37 29 36 34 19 32 12 37 15 46 24 49 28 36 9 29 5 45 23 27 4 23 16 39 22 22 2 46 20 47 17 18 25 13 38 42 38 45 28 37 19 5 10 30 49 16 18 46 47 47 3 39 23 41 20 6 40 21 22 40 49 20 1 13 36 12 40 12 15 24 20 21 12 19 25 8 48 22 37 33 1 39 30 50 50 8 35 38 31 24 40 23 7 20 34 35 6 19 27 41 1 14 50 17 42 6 35 12 48 30 29

It is an array with 3 dimensions, which are respectively 5 years, 2 assembly lines, and 12 months.

Suppose we now want to reorganise it into an array of 2 assembly lines, 5 years, and 12 months: a dyadic transposition can do that for us.

The left argument of dyadic transpose specifies the position that you want each dimension to appear in the result.

The shape of prod was

prod
5 2 12

but now we want it to become 2 5 12. The years should become the 2nd dimension, the assembly lines should become the 1st dimension, and the months should remain the 3rd dimension, hence the transposition vector will be 2 1 3:

2 1 3prod
9 16 42 50 2 3 11 45 17 37 29 36 29 5 45 23 27 4 23 16 39 22 22 2 37 19 5 10 30 49 16 18 46 47 47 3 13 36 12 40 12 15 24 20 21 12 19 25 38 31 24 40 23 7 20 34 35 6 19 27 34 19 32 12 37 15 46 24 49 28 36 9 46 20 47 17 18 25 13 38 42 38 45 28 39 23 41 20 6 40 21 22 40 49 20 1 8 48 22 37 33 1 39 30 50 50 8 35 41 1 14 50 17 42 6 35 12 48 30 29

Alternatively, if we want to reorganise it into an array of 2 assembly lines, 12 months, and 5 years, the method will be the same:

The required new shape is 2 12 5.

When all dimensions are different from each other, the transposition vector can be generated using dyadic iota:

 tv  2 12 5  prod
3 1 2
tvprod
9 29 37 13 38 16 5 19 36 31 42 45 5 12 24 50 23 10 40 40 2 27 30 12 23 3 4 49 15 7 11 23 16 24 20 45 16 18 20 34 17 39 46 21 35 37 22 47 12 6 29 22 47 19 19 36 2 3 25 27 34 46 39 8 41 19 20 23 48 1 32 47 41 22 14 12 17 20 37 50 37 18 6 33 17 15 25 40 1 42 46 13 21 39 6 24 38 22 30 35 49 42 40 50 12 28 38 49 50 48 36 45 20 8 30 9 28 1 35 29
_images/APLer-Skilled_Orange.png

Fig. 8.1 Skilled APLer that is confident with their tools.#

8.7. Exercises#

Exercise 8.1

Given the matrix xg1 below, try to produce each of the three following matrices; first using only take, and then only drop.

5 3 6
8 2 3
5 4 8 2
7 7 6 2
9 5 3
4 8 2
7 6 2
 xg1  3 51 9 5 3 6 5 4 8 2 3 7 7 6 2 6
1 9 5 3 6 5 4 8 2 3 7 7 6 2 6

Exercise 8.2

With xg1 again, how could you produce this:

1 9 5 3 6 0
5 4 8 2 3 0
7 7 6 2 6 0
0 0 0 0 0 0

Exercise 8.3

Write two dyadic functions TakeShape and DropShape. TakeShape computes ⍴ns↑array and DropShape computes ⍴ns↓array. The left argument to both functions will be ns and the right argument will be ⍴array. You can assume that the left argument and right arguments are valid and make sense together.

2 ¯3 TakeShape 1 5 10
2 3 10
15 4 ¯3 DropShape 10 10 10
0 6 7

Exercise 8.4

Write two dyadic functions, TakeAsDrop and DropAsTake, that take an array on the right and a vector on the left. r v TakeAsDrop a is such that (r↓a)≡v↑a whenever possible, and conversely for DropAsTake: r v DropAsTake is such that (r↑a)≡v↓a whenever possible.

Test your functions with your solutions to the first exercise.

Exercise 8.5

Write a function which “highlights” all the vowels of a given character vector by placing an arrow under them.

ShowVowels 'This function works properly'
This function works properly ↑ ↑ ↑↑ ↑ ↑ ↑ ↑

Exercise 8.6

Write two monadic functions:

  • Contract takes a sparse matrix and returns a more compact representation of the same matrix; and

  • Restore takes a compact representation of a sparse matrix and builds the full matrix.

For reference, a sparse matrix is a matrix that contains mostly zeroes, like the matrix xg6 shown below:

xg6  4 70
xg6[(1 3)(1 6)(2 2)(3 1)(3 3)(3 7)(4 5)]  8 3 7 6 2 1 4
xg6
0 0 8 0 0 3 0 0 7 0 0 0 0 0 6 0 2 0 0 0 1 0 0 0 0 4 0 0

A large sparse matrix may occupy a lot of memory. To reduce the memory consumption, we can consider an alternative, more compact representation. For example, we can start by ravelling the matrix:

,xg6
0 0 8 0 0 3 0 0 7 0 0 0 0 0 6 0 2 0 0 0 1 0 0 0 0 4 0 0

Then, we retain only the non-zero values together with their position in the ravel:

values     8 3 7  6  2  1  4
positions  3 6 9 15 17 21 26

Now, if we add the shape of the matrix on the left, we have all the necessary information to restore the original matrix when required:

  4 8 3 7  6  2  1  4  ⍝ values
  7 3 6 9 15 17 21 26  ⍝ positions
⍝ ^ shape of the original matrix
Contract xg6
4 8 3 7 6 2 1 4 7 3 6 9 15 17 21 26
xg6  Restore Contract xg6
1

Exercise 8.7

Write a dyadic function Whiten that replaces all occurrences of the character scalar left argument in the character vector right argument with blanks.

'a' Whiten 'Panama is a canal between Atlantic and Pacific'
P n m is c n l between Atl ntic nd P cific

Can you find a solution that uses the function expand?

Exercise 8.8

Write a dyadic function OnTop to centre a title above a character matrix.

monMat  6 8'January FebruaryMarch   April   May     June    '
'2022' OnTop monMat
2022 -------- January February March April May June

Exercise 8.9

Determine the result of the expression ¯3 ¯1 3⌽(-2 1 0 1 0 2 1 2 0)⊖xg9 for the matrix xg9 given below.

 xg9  3 9'oeornlhtu n siduothf uogYti'
oeornlhtu n siduot hf uogYti

Exercise 8.10

Write a dyadic function Free which looks for a sequence of contiguous zeroes in a Boolean vector. The right argument is the Boolean vector in which to look for the sequence of zeroes and the left argument is the integer that specifies the sequence length to look for. The function should return the position of the first zero of the first such sequence and, if no such sequence exists, it should return 0.

 xg10  1 0 0 1 1 1 0 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0
1 0 0 1 1 1 0 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0
1 Free xg10
0
2 Free xg10
2
3 Free xg10
10
4 Free xg10
15
6 Free xg10
0

Exercise 8.11

Write a dyadic function ToColumns that takes an integer scalar left argument and a character matrix right argument. The function should split the matrix into as many columns as the left argument specifies. Columns should be separated by, at least, a blank.

 xg11  11 8'Emily   Luciano Paul    Oxana   Thor    Carmen  VeronicaWilliam VladimirMonica  Colette '
Emily Luciano Paul Oxana Thor Carmen Veronica William Vladimir Monica Colette
3 ToColumns xg11
Emily Thor Vladimir Luciano Carmen Monica Paul Veronica Colette Oxana William

Exercise 8.12

Write a dyadic function ShowCodes that takes a numeric vector left argument and a character matrix right argument. The function should display the numerical codes on the left of the rows of the argument matrix, but with a blank row every time the code changes.

Notice that, in the example below, the vector xg12 with the codes has length equal to the number of rows in xg11.

xg12  22 22 74 74 74 74 30 65 65 65 19
xg12 ShowCodes xg11
22 Emily 22 Luciano 74 Paul 74 Oxana 74 Thor 74 Carmen 30 Veronica 65 William 65 Vladimir 65 Monica 19 Colette

8.8. The Specialist’s Section#


Each chapter is followed by a "Specialist's Section" like this one. This section is dedicated to skilled APLers who wish to improve their knowledge. You will find here rare or complex usages of the concepts presented in this chapter, or discover extended explanations which need the knowledge of some symbols that will be seen much further in the book.

If you are exploring APL for the first time, skip this section and go to the next chapter.

8.8.1. More About Laminate#

Here is a formal definition of the conditions required to laminate two variables a and b.

In the expression r a,[axis]b,

  • it is mandatory that (⍴a)≡(⍴b), unless one of them is a scalar;

  • axis must be a value between ⎕IO-1 and ⎕IO+≢⍴a; and

  • the shape of r is given by the expression ((⌊axis↑⍴a),2,((⌈axis)↓⍴a).

Examine the second rule: it is obvious that axis can be negative if the index origin is set to 0. The axis can also be negative for mix and ravel with axis.

Let us use the following two vectors:

a  41 27 88 11
b  39 63 12 69

In order to produce the matrix

2 4a,b
41 27 88 11 39 63 12 69

we write

a,[0.5]b
41 27 88 11 39 63 12 69

when ⎕IO 1, but if we set

⎕IO  0

then the expression becomes

a,[¯0.5]b
41 27 88 11 39 63 12 69
⎕IO  1

8.8.2. More on Dyadic Transpose#

8.8.2.1. Conditions#

We said that dyadic transpose can be thought of as a way to observe an array from different positions. For this usage there is a certain rule to follow.

To transpose an array with a⍉b, we must have a[⍋a]≡⍳≢⍴b.

In other words, a must be composed of all the values of ⍳≢⍴b taken in any order, or, mathematically speaking, a must be a permutation of the axes of b.

8.8.2.2. Diagonal Sections of an Array#

Dyadic transpose can also be used to select the items from an array which have two or more identical coordinates. Such selections are called “diagonal sections” of the array.

For example, let us use the following array:

 y  3 3 4⍴⍳36
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

Look at the result of the following expression:

1 1 2y
1 2 3 4 17 18 19 20 33 34 35 36

We have specified that both the first and second dimensions are to become the first dimension of the result. This conflict is resolved by extracting the items on the diagonal between the dimensions which are merged.

The items on this diagonal have identical first and second coordinates, much like the identical nature of the first and second values in the left argument 1 1 2⍉y. The result is a section of the “cube”.

The expression 2 2 1⍉y would give the same result, but transposed:

2 2 1y
1 17 33 2 18 34 3 19 35 4 20 36
(1 1 2y)≡⍉2 2 1y
1

Rules

This special use of a⍉b must also follow some rules:

  • (≢a)≡(≢⍴b)

  • ∧/a∊⍳≢⍴b

  • ∧/(⍳⌈/a)∊a

The first two rules are not unique to this special usage of dyadic transpose, and the third rule means that the items of a must be consecutive integers, starting from ⎕IO.

So, for the array y shown above, the only possible sections are:

(1 1 1)(1 1 2)(1 2 1)(1 2 2)(2 1 1)(2 1 2)(2 2 1)(⊣,[.5]¨)y
┌───────┬───────────┬────────┬────────┬────────┬───────┬───────┐ │1 1 1 │1 1 2 │1 2 1 │1 2 2 │2 1 1 │2 1 2 │2 2 1 │ ├───────┼───────────┼────────┼────────┼────────┼───────┼───────┤ │1 18 35│ 1 2 3 4│ 1 5 9│ 1 6 11│ 1 13 25│1 14 27│1 17 33│ │ │17 18 19 20│14 18 22│13 18 23│ 6 18 30│5 18 31│2 18 34│ │ │33 34 35 36│27 31 35│25 30 35│11 23 35│9 22 35│3 19 35│ │ │ │ │ │ │ │4 20 36│ └───────┴───────────┴────────┴────────┴────────┴───────┴───────┘

If the required conditions are satisfied, the selection can be thought of as having two steps:

  1. the left argument a is examined to see which of the coordinates are identical.

For example, a left argument equal to 1 2 1 or 2 1 2 will select items which have their first and their coordinates identical. A left argument equal to 1 1 2 or 2 2 1 will select items which have their first two coordinates identical.

1 2 2⍉y and 2 1 1⍉y would select the items which have their last two coordinates equal, that is,

 1  6 11
13 18 23
25 30 35

So, now we know which items from y we must work with in the next step.

  1. the selected values are repositioned using a normal dyadic transposition, the left argument of which is composed of the unique values of a obtained by ∪a.

For example, we said that both 1 2 2⍉y and 2 1 1⍉y would select the same set of values, shown above. This little matrix will then be transposed using a left argument equal to 1 2 (the matrix remains unchanged) or 2 1 (the matrix is transposed):

1 2 2y
1 6 11 13 18 23 25 30 35
2 1 1y
1 13 25 6 18 30 11 23 35

We recommend that you try to work through the other possibilities to make sure you understand them.

8.8.2.3. Diagonal Section of a Matrix#

In the case of a matrix, the only possible diagonal section is specified by 1 1. It selects what is called the main diagonal of the matrix.

For example, here is how you would select the values along the main diagonal of forecast:

1 1forecast
90 450 320 520
forecast
90 160 420 500 20 30 110 450 170 370 290 360 340 190 320 120 510 370 150 460 240 520 490 280

Notice that for a matrix m, we have (≢1 1⍉m)=⌊/⍴m. This means that the diagonal section of a rectangular matrix has as many elements as the shorter side of the matrix, like in the forecast example above.

8.9. Solutions#

The following solutions we propose are not necessarily the “best” ones; perhaps you will find other solutions that we have never considered. APL is a very rich language, and due to the general nature of its primitive functions and operators there are always plenty of different ways to express different solutions to a given problem. Which one is “the best” depends on many things, for example the level of experience of the programmer, the importance of system performance, the required behaviour in border cases, the requirement to meet certain programming standards and also personal preferences. This is one of the reasons why APL is so pleasant to teach and to learn!

We advise you to try and solve the exercises before reading the solutions!

Solution to Exercise 8.1

Starting from xg1,

 xg1  3 51 9 5 3 6 5 4 8 2 3 7 7 6 2 6
1 9 5 3 6 5 4 8 2 3 7 7 6 2 6

we want to extract sub-matrices using take and drop. Because we are dealing with a matrix – a rank 2 array – the left argument must be a 2-element vector. The first element determines how many rows we take (or drop) and from where (top or bottom), while the second element determines how many columns we take (or drop) and from where (left or right).

Let us go through the three matrices, one by one:

1.

5 3 6
8 2 3

This matrix has elements from the first two rows and from the last three columns of xg1, so we can build it with

2 ¯3xg1
5 3 6 8 2 3

Conversely, we can build that matrix by ignoring the last row and the first two columns:

¯1 2xg1
5 3 6 8 2 3

2.

 5 4 8 2
7 7 6 2

This matrix has elements from the last two rows and from the first four columns, so we build it with

¯2 4xg1
5 4 8 2 7 7 6 2

Conversely, we can build it by ignoring the first row and the last column of xg1:

1 ¯1xg1
5 4 8 2 7 7 6 2

3.

9 5 3
4 8 2
7 6 2

Finally, this matrix contains all three rows but the 3 columns that make it up are from the middle of xg1, so there is no single number that we can use to build this matrix in a single take. Instead, what we can do is two successive takes: first we take the first 4 columns and then the last 3 columns of that, or first we take the last 4 columns and then the first 3 of that:

3 33 ¯4xg1
9 5 3 4 8 2 7 6 2

We do a similar thing with drop:

0 10 ¯1xg1
9 5 3 4 8 2 7 6 2

Solution to Exercise 8.2

We want to produce the matrix

1 9 5 3 6 0
5 4 8 2 3 0
7 7 6 2 6 0
0 0 0 0 0 0

from xg1. We immediately notice that xg1 is in the upper-left corner of this larger matrix, and we see there are an extra row and column of zeroes, which are certainly the result of overtaking:

4 6xg1
1 9 5 3 6 0 5 4 8 2 3 0 7 7 6 2 6 0 0 0 0 0 0 0

Solution to Exercise 8.3

Let us start with TakeShape, that is supposed to compute the shape of the result of doing ns↑array.

r ns TakeShape ⍴array should be such that r≡⍴ns↑array, and notice that the right argument to TakeShape is ⍴array, not the array itself.

From reading the specification of take and from the previous exercises, we can start to see that the absolute value of the left argument indicates the length of each dimension. However, the solution isn’t just

TakeShape  {|}

Why not? Because the left argument to take may be shorter than the number of dimensions of the array we are working with, in which case the trailing dimensions are left untouched. To fix our function, we just need to check if there are trailing dimensions that are left untouched and, in case there are any, we should tack them to the right of |ns:

TakeShape  {(|),(()-≢)}
2 ¯3 TakeShape 1 5 10
⍝ 2 3 10
2 3 10

How does it work?

  • ≢⍵ is the number of dimensions of the array and ≢⍺ is the number of dimensions of the left argument to take, so (≢⍵)-≢⍺ is the number of trailing dimensions that are left untouched;

  • we then use take to reach the lengths of those dimensions, so they show up in the final result. However, we want to take from the end, so we would need -(≢⍵)-≢⍺; instead, we flip the subtraction in the first place to get (≢⍺)-≢⍵; and

  • we tack those trailing dimensions to the absolute value of the left argument.

The problem statement specifies that the arguments to TakeShape will always be relative to a usage of take that makes sense, so we also know that (≢⍺)-≢⍵ is always 0 or negative.

We can follow a similar train of thought with DropShape, except that now doesn’t tell us the length of each dimension, but rather the number of elements that are dropped along that dimension. For DropShape, we can take advantage of overtaking to make sure that the left argument has the same length as the right argument. Finally, we just need to make sure we don’t try to drop too many elements along any dimension, because the final result cannot have dimensions with negative length:

DropShape  {0-|()}
15 4 ¯3 DropShape 10 10 10
⍝ 0 6 7
0 6 7

Solution to Exercise 8.4

This exercise wants us to convert left arguments of take into left arguments of drop and vice-versa, and doing so means we need to understand the lengths of the resulting dimensions, but that is what we have been doing in the previous exercises.

Start by imagining how it works for a vector v of length l.

  • if we write t↑v, for positive t, the corresponding left argument for drop would be -l-t; but

  • if we write (-t)↑v, then the corresponding left argument for drop would be l-t.

Now we just need to find a way of unifying the two cases:

TakeAsDrop  {()×-|}

The expression ⍵-|⍺ determines the magnitude of the corresponding drop left argument and the expression (-×⍺) corrects the sign.

This is a great starting point, but our implementation has three issues that we need to solve. One of them should become apparent here:

10 TakeAsDrop 5
5

If we try to take 10 elements from a vector of length 5, we end up with a (larger) vector of length 10, so there is no left argument to drop that would have the same effect. Arguably, the most sensible thing to do here would be to return 0, because that is the left argument for a drop that would have the closest result possible to the original take. To make this change, what we need to do is make sure that the magnitude of the argument to drop is never below zero:

TakeAsDrop  {()×0-|}
10 TakeAsDrop 5
0

The second issue, that really is a bug, has to do with what happens if we take 0 elements:

0 TakeAsDrop 5
0

If we take 0 elements from a vector of length 5, that would be the same as dropping 5 elements from that vector. However, we say it is the same as dropping 0 elements from that vector, which is wrong. The problem is that (-×⍺) evaluates to 0 in this case, whipping out the correct magnitude computed by 0⌈⍵-|⍺. One way to fix that is by nudging the values of slightly, so that their signs remain the same for all integers, except for 0:

TakeAsDrop  {(-0.5)×0-|}
0 TakeAsDrop 5
5

The third and final issue, is that TakeAsDrop doesn’t handle cases where the left argument is shorter than the right argument:

3 4 TakeAsDrop 10 8 6
LENGTH ERROR: Mismatched left and right argument shapes
TakeAsDrop[0] TakeAsDrop←{(-×⍺-0.5)×0⌈⍵-|⍺}
                                       ∧

The simplest way to fix this is by shortening to match the length of . This works because the trailing dimensions that are not covered by the left argument to take can also be omitted in the left argument to drop:

TakeAsDrop  {(-0.5)×0(())-|}
3 4 TakeAsDrop 10 8 6
¯7 ¯4

As for the DropAsTake function… It is exactly the same as the TakeAsDrop one!

Solution to Exercise 8.5

It is clear that an integral part of this exercise will be finding the vowels in the given character vector, and we can use the membership primitive for that:

'This function works properly''aeiouyAEIOUY'
0 0 1 0 0 0 1 0 0 0 1 1 0 0 0 1 0 0 0 0 0 0 1 0 1 0 0 1

Next, we want to turn that Boolean mask into a character vector with arrows where the ones are. If we take advantage of the fact that the blank space ' ' is the fill element for character vectors, we can use the expand primitive that we just learned about:

('This function works properly''aeiouyAEIOUY')\'↑'
↑ ↑ ↑↑ ↑ ↑ ↑ ↑

To wrap it up, all we need to do is laminate this character vector with the original vector:

ShowVowels  {,[.5]('aeiouyAEIOUY')\'↑'}
ShowVowels 'This function works properly'
This function works properly ↑ ↑ ↑↑ ↑ ↑ ↑ ↑

Solution to Exercise 8.6

Let’s work with the following matrix:

xg6  4 70
xg6[(1 3)(1 6)(2 2)(3 1)(3 3)(3 7)(4 5)]  8 3 7 6 2 1 4
xg6
0 0 8 0 0 3 0 0 7 0 0 0 0 0 6 0 2 0 0 0 1 0 0 0 0 4 0 0

In order to contract the matrix, we start by raveling it:

,xg6
0 0 8 0 0 3 0 0 7 0 0 0 0 0 6 0 2 0 0 0 1 0 0 0 0 4 0 0

And we also list all the indices of all the positions in the matrix:

⍳≢,xg6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

What we now want is to keep only the indices of non-zero values. If we start by laminating the numbers and the positions together, we can then use a Boolean mask to figure out what things to keep:

xg6r  ,xg6
(xg6r0)/xg6r,[.5]⍳≢xg6r
8 3 7 6 2 1 4 3 6 9 15 17 21 26

The final step is catenating the original shape of the matrix on the left:

]dinput
Contract  {
    r  ,
    (),(r0)/r,[.5]⍳≢r
}
Contract xg6
4 8 3 7 6 2 1 4 7 3 6 9 15 17 21 26

In order to implement the function Restore, we can create an empty ravel of the matrix which we then populate with the correct values. After populating, we just need to reshape the ravel into the original matrix by making use of the shape information that is on the first column of the input:

]dinput
Restore  {
    shape  [;1]
    ravel  (×/shape)0
    ravel[1 1]  ¯1 1
    shaperavel
}
Restore Contract xg6
0 0 8 0 0 3 0 0 7 0 0 0 0 0 6 0 2 0 0 0 1 0 0 0 0 4 0 0

Solution to Exercise 8.7

When the problem statement mentions blanks and expand, it hints at the fact that the blank space is the fill element for character vectors, and we know that expand inserts fill elements when it finds zeroes.

A first hunch might be that we just need to create a Boolean mask that has a 1 for each character we want to keep, and then we use expand with that, but unfortunately that does not work:

sentence  'Panama is a canal between Atlantic and Pacific'
(sentence'a')\sentence
LENGTH ERROR
      (sentence≠'a')\sentence
                    ∧

The problem is that the left argument to expand needs to have as many positive elements as there are elements in the right argument. In order to fix this, what we can do is remove the letter we want to replace with blanks and only then use expand. We can remove the occurrences of the letter with a compress or with the without primitive:

Whiten  {()\~}
'a' Whiten sentence
P n m is c n l between Atl ntic nd P cific
]dinput
Whiten  {
    m  
    m\m/
}
'a' Whiten sentence
P n m is c n l between Atl ntic nd P cific

Solution to Exercise 8.8

If we want to centre something, that means we will need to rotate it to the correct place. When we receive the title, we first pad it with enough blank spaces so that it can be catenated on top of the matrix:

padded  (1↓⍴monMat)'2007'
padded
8

We overtake to achieve this effect. The next thing is rotating some of the trailing spaces to the front, so that the title becomes centred. To do that, we need to figure out how much larger the matrix is in relation to the title, and then we divide that by 2 and round, in case there are an odd number of spaces:

rot  ((1↓⍴monMat)-≢'2007')÷2

Now we can rotate the padded title by minus that amount (otherwise we would be rotating in the wrong direction):

(-rot)padded
2007

Now it is just a matter of catenating this to the top of the matrix, together with the - underline. Let us put everything together in a dfn:

]dinput
OnTop  {
    c  1↓⍴
    rot  (c-≢)÷2
    padded  c
    ((-rot)padded)'-'
}
'2022' OnTop monMat
2022 -------- January February March April May June

Notice that rounding up or down changes the position of the extra space when there are an odd number of spaces around the title:

'107' OnTop monMat
107 -------- January February March April May June

Solution to Exercise 8.9

Best way to check if your solution is correct is by actually running the code:

 xg9  3 9'oeornlhtu n siduothf uogYti'
oeornlhtu n siduot hf uogYti
¯3 ¯1 3(-2 1 0 1 0 2 1 2 0)xg9
You found the right solution

Solution to Exercise 8.10

Let us prototype our solution with the vector that follows, and let us assume we want to find a run of 4 zeroes.

 xg10  1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0
1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0

We can start by asking where in our vector the 1s are:

xg10
1 4 5 6 8 12 13 18 19 24

This is useful because we know that the 0s are between the 1s.

However, if our vector starts and/or ends with 0s, those will not be enclosed by 1s and so we need to prepend and append a 1 to be sure all runs of 0s are surrounded by 1s:

 ones  1,xg10,1
1 2 5 6 7 9 13 14 19 20 25 28

The next thing we do is figure out the distances between the consecutive 1s, so that we know the lengths of the runs of 0s:

 runLengths  ¯1+(1ones)-(¯1ones)
0 2 0 0 1 3 0 4 0 4 2

Now we just use the lengths of the runs to compress the initial ones vector. However, we can’t do it directly:

(4=runLengths)/ones
LENGTH ERROR
      (4=runLengths)/ones
                    ∧

The problem is that the vector ones contains the beginning and end of each run of zeroes, whereas the vector runLengths only has the corresponding length. In short, ones contains one element too many, that corresponds to the 1 that was catenated to the end of the argument vector. So, we need to drop the last element of ones before compressing:

(4=runLengths)/¯1ones
14 20

Finally, we take the first element of that:

1(4=runLengths)/¯1ones
14

At this point, we can figure out the approximate location of our runs of zeroes, we just need to make sure that we return the exact index required by the problem statement. The problem statement wanted us to return the index of the first 0 of such a run. We worked with indices of 1s, and not 0s, but we also catenated a 1 at the beginning of the vector, thus shifting all the indices of the 1s. This means that, even though we are working with indices of 1s, the final result matches the corresponding index of the first 0.

Let’s wrap our code in a dfn:

]dinput
Free  {
    ones  1,,1
    runLengths  ¯1+(1ones)-(¯1ones)
    1(=runLengths)/¯1ones
}

What if the right argument does not have a run with the required length? Does our function return 0 like the problem statement requires?

6 Free xg10
0

It does… But how?

When we filter for runs of the required length, we get nothing, because there are no such runs:

(6=runLengths)/¯1ones

In other words, we get an empty numeric vector:

(6=runLengths)/¯1ones
1

If our vector is empty, then trying to use take to get its first element corresponds to over taking, because we are trying to retrieve too many elements. When that is the case, we know that the function take will put as many fill elements as needed, and so it returns a single 0 as a result of doing “one take” on an empty vector:

1
0

Here is an alternative solution. First, we stack the vector on top of itself as many times as required by the desired run length (let us say it is n 4):

n  4
 mat  n (xg10)xg10
1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0

Next, we use rotate to rotate each row one more element than the previous one, so that each column represents n consecutive elements of the vector:

 rot  (¯1+⍳n)mat
1 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 0 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 1 1 1 0 1 0 0 0 1 1 0 0 0 0 1 1 0 0 0 0 1 0 0 1 0 0

In other words, if you look at the top row, you will notice that the 3 elements below a number match the 3 elements to its right. For example, the 4th element in the 1st row is

rot[1;4]
1

The 3 elements to its right are

rot[1;5 6 7]
1 1 0

and the three elements below it are

rot[2 3 4;4]
1 1 0

Now, the runs that we care about correspond to columns that are completely 0. Using a reduction, we can figure out which columns have 1s and which do not:

rot
1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 1 1 1 0 1 1 1 1 1 1

To finish up, we can do something similar to what we did before:

1↑⍸~∨rot
14

This is what this solution would look like in a dfn:

]dinput
Free2  {
    mat   ()
    rot  (¯1+⍳)mat
    1↑⍸~∨rot
}
3 Free2 xg10
9
6 Free2 xg10
0
4 Free2 xg10
14

Solution to Exercise 8.11

We start with our matrix of names:

 xg11  11 8'Emily   Luciano Paul    Oxana   Thor    Carmen  VeronicaWilliam VladimirMonica  Colette '
Emily Luciano Paul Oxana Thor Carmen Veronica William Vladimir Monica Colette

Let us also assume we want to make n 3 columns:

n  3

If we look at the problem statement, we see that the names must be laid out top to bottom, and only then left to right. However, in APL, the ravel order of arrays is first left to right, and then top to bottom. So it seems like we have to fight APL’s natural order in some way… The key to doing that is understanding that we actually have 3 dimensions along which we want to navigate:

  1. we want to walk along one dimension that holds a single name,

  2. then we want to go down along the column of names,

  3. and then we want to go right along the columns of names.

This hints at the fact that we should reshape our data to have 3 dimensions, where one dimension holds the columns, another holds all the names, and the last one holds the characters that compose a name.

The first thing that we need to do, in order to break up the matrix with the names, into an array with 3 dimensions, is figure out the length of each dimension. n 3 indicates that we want 3 columns, but how many names are there in each slice?

(height width)  xg11
height÷n
3.66667

This doesn’t divide evenly, so we use the ceiling primitive to round up, and then we use take to create empty names to pad the column:

colSize  height÷n
 names  (colSize×n)xg11
Emily Luciano Paul Oxana Thor Carmen Veronica William Vladimir Monica Colette

Maybe you didn’t notice, but now there is an empty row added to the matrix. So, the next step is reshaping the matrix names into a 3D array:

 columns  n colSize width  names
Emily Luciano Paul Oxana Thor Carmen Veronica William Vladimir Monica Colette

Now we have an array where the dimensions match the dimensions we want, but now we need to reorder the dimensions so that they do what we want when we reshape the 3D array back into a matrix. We see that the 3rd dimension is already correct, because the names are laid out along the 3rd (last) dimension. This means the only thing we can try to do is reorder the first and second dimensions:

2 1 3columns
Emily Thor Vladimir Luciano Carmen Monica Paul Veronica Colette Oxana William

After the reordering of the two leading dimensions, we can now reshape the 3D array into a matrix:

colSize (n×width)2 1 3columns
Emily Thor Vladimir Luciano Carmen Monica Paul VeronicaColette Oxana William

This is almost perfect, we just need to add blanks between all the columns. In order to do so, we can catenate a blank space at the end of the array (making it one character wider) and then dropping the last column of superfluous blanks:

0 ¯1colSize (n×width+1)2 1 3columns,' '
Emily Thor Vladimir Luciano Carmen Monica Paul Veronica Colette Oxana William

This can be written as a dfn:

]dinput
ToColumns  {
    (h w)  
    nh  h÷
    names  (nh×)
    columns   nh wnames
    0 ¯1nh (×w+1)2 1 3columns,' '
}
3 ToColumns xg11
Emily Thor Vladimir Luciano Carmen Monica Paul Veronica Colette Oxana William

For reference, we provide another implementation that is not correct, where we just do some reshaping. In this alternative solution, the names get listed from left to right, and then from top to bottom.

]dinput
ToColumnsWrong  {
    (h w)  
    nh  h÷
    0 ¯1nh(×w+1)(×nh×w+1)↑,xg11,' '
}
3 ToColumnsWrong xg11
Emily Luciano Paul Oxana Thor Carmen Veronica William Vladimir Monica Colette

Solution to Exercise 8.12

For this exercise, we are given an extra vector xg12:

xg12  22 22 74 74 74 74 30 65 65 65 19

It is fairly straightforward to put the labels next to the names:

 labeled  xg12,' ',xg11
22 Emily 22 Luciano 74 Paul 74 Oxana 74 Thor 74 Carmen 30 Veronica 65 William 65 Vladimir 65 Monica 19 Colette

The final step is creating the blank lines that separate the different labels.

For that, we need to create a Boolean vector that we can give to expand. This is what we need to create:

1 1 0 1 1 1 1 0 1 0 1 1 1 0 1labeled
22 Emily 22 Luciano 74 Paul 74 Oxana 74 Thor 74 Carmen 30 Veronica 65 William 65 Vladimir 65 Monica 19 Colette

In order to create that vector, we will want to figure out the indices of the above vector that are 0. To do so, we start by finding where the labels change:

 indices  (¯1xg12)1xg12
2 6 7 10

However, if we look at the Boolean vector above, these are the indices of the 0s:

⍸~1 1 0 1 1 1 1 0 1 0 1 1 1 0 1
3 8 10 14

Can you see the pattern?

Here is the mapping that we need to do:

  • 2 goes to 3

  • 6 goes to 8

  • 7 goes to 10

  • 10 goes to 14

indices+⍳≢indices
3 8 10 14

Every index that we found in indices points to the last position of a run of labels, and we wanted to insert a new 0 in the position after the run ends and before the next run begins. However, if we imagine we are working from the left, every time we insert a new position between different labels, we shift part of the vector to the right. That is why we need to add the iota of the tally of indices, to correct for those shifts.

Now we use these indices to create the Boolean vector we need:

 exp  ~((indices)+≢xg12)indices+⍳≢indices
1 1 0 1 1 1 1 0 1 0 1 1 1 0 1
explabeled
22 Emily 22 Luciano 74 Paul 74 Oxana 74 Thor 74 Carmen 30 Veronica 65 William 65 Vladimir 65 Monica 19 Colette
]dinput
ShowCodes  {
    labeled  ,' ',
    indices  (¯1)1
    exp  ~((indices)+≢xg12)indices+⍳≢indices
    explabeled
}
xg12 ShowCodes xg11
22 Emily 22 Luciano 74 Paul 74 Oxana 74 Thor 74 Carmen 30 Veronica 65 William 65 Vladimir 65 Monica 19 Colette