Some Primitive Functions
Contents
4. Some Primitive Functions#
4.1. Definitions#
In APL, data is processed using what we call functions. It is important to distinguish between two types of functions:
Primitive Functions:
they are part of the APL language;
they are represented by symbols like
⍴
,⍉
and⌈
; andthey cannot be overwritten or removed.
User-Defined Functions:
as their name implies, they are written by the user;
they are represented by names, for example
Average
orBudget
; andthey can be overwritten and removed.
APL has a very rich set of primitive functions. In this chapter, we will explore just a few of them; many others will follow in subsequent chapters.
In the introduction to this book, we mentioned that in traditional mathematics, some symbols can be used with a single argument or two arguments. For example, in the expression \(a = x \color{red}{-} y\) the minus sign means subtract, whereas in \(a = \color{red}{-}y\) the minus sign indicates the negation of \(y\).
The first form is called the “dyadic” use of the symbol. The second form is called the “monadic” use of the symbol.
It is the same in APL, where most of the symbols (functions) have a monadic and a dyadic meaning. For example, here ⍴
obtains the shape of the 1 2 3 4
vector:
⍴ 1 2 3 4
Whereas in here ⍴
changes the shape of the 1 2 3 4
vector to 2 2
:
2 2 ⍴ 1 2 3 4
There is, however, a major difference. In traditional mathematics, the symbol representing a monadic function is sometimes placed before its argument (as the \(-\) in \(a = \color{red}{-}y\)), sometimes after it (as the \(!\) in \(a = y\color{red}{!}\)), sometimes on both sides (as the \(|\cdot|\) in \(a = \color{red}|y\color{red}|\)), and some other conventions may be found.
In APL, the symbol representing a monadic function is always placed before its argument, as the ⍴
in ⍴var
.
4.2. Some Scalar Dyadic Functions#
4.2.1. Definition and Examples#
Scalar dyadic functions are primitive functions which have the following properties:
they are dyadic (require an argument on both sides);
they work item by item (scalar by scalar);
they can work on two arrays of the same shape, in which case the result also has the same shape; and
they can work on one array of any shape, and a single value (a scalar or any one-item array), in which case the result has the same shape as the non-singleton array.
The four basic arithmetic functions, addition, subtraction, multiplication, and division are scalar dyadic functions. They apply themselves between each item of the left argument and the corresponding item of the right argument, like this:
5 3 2 9 + 2 6 8 4
The function is applied between each item of two 4-item vectors. The result is also a 4-item vector.
As an example of a function that is not a scalar function, let us look at the reshape function. There is nothing in common between the shapes of its arguments:
2 3 ⍴ 6 8 2 1 9 3
In fact, the left argument has 2 items, the right one has 6 and the result in this case is a matrix.
Let us explore the behaviour of the basic arithmetic functions on vectors:
5 3 2 9 - 2 6 8 4
5 3 2 9 ÷ 2 6 4 7
price ← 5.2 11.5 3.6 4 8.45
qty ← 2 1 3 6 2
costs ← price × qty
costs
Scalar dyadic functions apply to arrays of any rank and shape.
As we saw in the introduction, a Sales Director makes forecasts for sales of 4 products over the coming 6 months, and assigns them to the variable forecast
:
⎕RL ← 73
forecast ← 10×?4 6⍴55
forecast
At the end of the 6 months, they record the actual values in the variable actual
. Here they are:
⎕RL ← 73
actual ← forecast + ¯10+?4 6⍴20
actual
Remark
We initialise the forecast
and actual
variables with some random values by the use of the function roll ?
. Notice this assignment is easier to type than some predefined set of values and we can use ⎕RL
to always get the same result. You can learn more about ?
and ⎕RL
later on in the book.
The first thing any self-respecting Sales Director will want to know is the difference between the expected and the actual results. This can be done easily by typing:
actual - forecast
Notice how subtracting two matrices gives a matrix of the same shape (recall that negative values are indicated by a high minus sign).
But remember, a scalar dyadic function may also be applied between a single value and an array of any shape.
For example, if we want to multiply forecast
by 2, we can type:
forecast × 2 ⍝ same as 2 × forecast
A complete list of scalar dyadic functions is given in Section 14.1.
4.2.2. Division By Zero#
An expression such as 17÷0
leads to an error message:
17÷0
DOMAIN ERROR: Divide by zero
17÷0
∧
This happens because zero does not belong to the domain of valid denominators.
However, notice what 0÷0
returns:
0÷0
Despite being mathematically incorrect, the default behaviour gives the result 1, as given by the extension of the rule that “any number divided by itself should give 1”. Nevertheless, because this is sometimes inappropriate, it is possible to change the default behaviour (see Section 4.19.1).
4.2.3. Power#
In APL, the mathematical notation \(x^n\) is written x*n
.
The function power (*
) accepts any value(s) for n
: integer or decimal, positive, negative, or zero, according to traditional usage.
To calculate the values of \(4^2\), \(4^{1.4}\), \(4^0\), \(\sqrt 4\), \(4^{-2.1}\), \(4^5\) we just need to type
4 * 2 1.4 0 0.5 ¯1 ¯2.1 5
0*0
gives 1
, which is also not mathematically correct but a fairly useful convention to take.
There is no special symbol in APL to represent a square root; it is obtained by raising a value to the power \(\frac12\). If we take the square root of a negative number, then we get a complex number as a result:
¯1 * 0.5
The subject of “complex numbers” pertains to mathematics, but Dyalog APL has some nice features related to complex numbers and so we will dive deeper into them later on.
4.2.4. Maximum & Minimum#
Maximum (⌈
) and minimum (⌊
) return respectively the larger of two values and the smaller of two values. Because they are scalar dyadic functions, they can be applied item by item between any two compatible arrays.
75 ⌈ 83
19 ⌈ 11 22 ¯20 60
52 14 ¯37 18.44 ⌊ ¯60 15 ¯40 11.23
Minimum can be used to apply a limit to the values in an array. For example, to set a ceiling of 450 in the matrix forecast
, it is sufficient to type:
forecast ⌊ 450
Notice how the larger values have been capped to 450.
4.2.5. Relationship#
As in traditional mathematics, APL provides the six relationship functions:
APL |
Meaning |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
These symbols are obtained by pressing the APL key, simultaneously with the keys 3 to 8, respectively.
All these functions return 1
if the relation is true, or 0
if it is false.
11 < 7
24 ≤ 24 11 33
5 = 9
3 8 7 ≥ 5 8 0
6 > 2 3⍴7 2 9 3 6 4
The results are called binary, or Boolean, values (Boolean refers to the name of the mathematician George Boole). They can be processed in many different ways and are extremely useful, as we shall soon see.
Note that none of the four symbols <
≤
≥
>
can be applied to character arrays. Only =
and ≠
can be used with character arrays, as illustrated below:
'm' = 'm'
'm' = 'M'
'k' ≠ 'a'
'sorry' ≠ 'r'
Because these functions are scalar dyadic functions, they are applied between individual scalars (the letters), not words:
'gold' ≠ 'gulf'
For the same reason, the two words (considered as vectors) must be of equal size, otherwise we get an error:
'male' ≠ 'female'
LENGTH ERROR: Mismatched left and right argument shapes
'male'≠'female'
∧
4.2.6. Residue#
The residue function, represented by |
, returns the remainder of a division.
In the expression R ← X|Y
, R
is the remainder of Y
divided by X
(be careful; the arguments of residue are given in the reverse order of that used by division Y÷X
).
7 | 54
2 | 216 47 29 28 ⍝ Find even and odd numbers
X ← 7 4 11 ¯4.3 3 ¯5 6 ¯3
Y ← 54 84 119 19.6 29 43 ¯14 ¯14
X | Y
The function can be used with negative and decimal values, as seen above.
The result R
is always equal to Y - (N×X)
, where N
is the largest possible integer such that R
is always between 0
and X
, but never equal to X
. You can show that N
is equal to ⌊(Y÷X)
, which means X|Y
and Y - (X × (⌊(Y÷X)))
give the exact same result:
(X|Y)
(Y - (X × (⌊(Y÷X))))
Using a new dyadic primitive ≡
(obtained with APL+Shift+;) we can ask APL to check if the two arrays are the same, instead of having to compare each array item by item:
(X|Y) ≡ (Y - (X × (⌊(Y÷X))))
Keep reading the next section to see how ≡
works.
4.2.7. Array Comparison#
We have seen above that the dyadic functions =
and ≠
are scalar, which meant we cannot use them when we wish to check if two arrays are the same or when we wish to check if two arrays are different:
1 2 3 4 = 1 2 3 4
The result above says the arrays have matching items, but doesn’t provide the summary information that the arrays are the same.
For a similar reason, we can’t even compare vectors of different lengths because dyadic scalar functions expect the shapes of their arguments to match:
'male' = 'female'
LENGTH ERROR: Mismatched left and right argument shapes
'male'='female'
∧
Instead, we must use the function match (≡
) to check if two arrays are exactly the same:
1 2 3 4 ≡ 1 2 3 4
'male' ≡ 'female'
The primitive not match (≢
) (obtained with APL+Shift+’) is the counterpart to ≡
and checks if two arrays are different:
1 2 3 4 ≢ 1 2 3 4
'male' ≢ 'female'
4.3. Order of Evaluation#
Like other programming languages, APL allows the programmer to use parentheses to specify the order of evaluation of a complex expression. Thus the expression 5×(6+7)
means “add 6 to 7, then multiply by 5”. In the absence of parentheses, most other programming languages employ rules of precedence to decide how a complex expression such as 5×6+7
would be evaluated. Typically, the result will be 37 because multiplication is given precedence over addition and is performed first.
When APL was designed, it was decided that the sheer number of primitive functions meant that a set of precedence rules would be impossibly complex to remember and apply.
The solution adopted in APL is simple, and consistent with the rules we apply to calculate complex expressions in traditional algebra. Suppose, for example, that we need to calculate
To do this, we would first divide \(x\) by 3, then take the square root of the result, next calculate its sine, and finally calculate the logarithm: each function applies to the result of the entire expression to its right. This is how it is done in mathematics, and so it is in APL. The only difference is that in APL there are no exceptions!
To evaluate
5 × 6 + 7
we first calculate
6 + 7
and then multiply by 5, giving 65:
5 × 13
By the use of parentheses we can instruct APL to do the multiplication first,
(5 × 6) + 7
which an experienced APL programmer would probably write as
7 + 5 × 6
Rule
In an APL expression, each function takes as its right argument the result of the entire expression to its right. No functions have higher precedence than any others.
If the function is dyadic (takes both a left and a right argument), it takes as its left argument the array immediately to its left, delimited by the next function.
This is sometimes called “right to left evaluation” (although this is not strictly correct).
If necessary, one can use parentheses to force a different order of evaluation.
You must not be confused: each function is itself evaluated in its natural order, so 8÷4
gives 2, not 0.5! The expression “right to left” only means that the first operation executed is the rightmost one.
If the order of evaluation seems strange to you at first sight, just refer to a plain English sentence: “take the top half of the bottom quarter” does not mean “take the top half first, and then take the bottom quarter”; it means “first split into quarters and take the bottom one, then split that quarter into two halves and take the top half of it”: this is exactly the way that APL works! Even in everyday English language, which we write from left to right, we implicitly use the “right to left evaluation” rule.
Let us apply this rule to some examples:
3×5+1
First we sum,
5+1
then we multiply:
3×6
Now an example with three operations:
3 6⌊4+2 9>7
First we perform the “greater than” comparison,
2 9>7
then we add 4
4+0 1
and finally we take the minimum:
3 6⌊4 5
An even more complex example, from the end of the subsection on the primitive function residue (|
):
(X|Y)≡Y-X×⌊Y÷X
Notice that in the example above we still kept a set of parentheses around X|Y
, because we want ≡
to compare X|Y
with Y-X×⌊Y÷X
. To get rid of those parentheses one would need to introduce an intermediate variable, for example like so:
R ← X|Y
R≡Y-X×⌊Y÷X
Warning
In the beginning, you may encounter some surprises when using commutative functions.
For example, if v
is a vector, 1+⍴v
is different from ⍴v+1
. Let us see why, with the following vector:
v ← 5 2 7
The value of
1+⍴v
is computed by first taking
⍴v
followed by
1+3
Whereas the value of
⍴v+1
is computed by first taking
v+1
followed by
⍴6 3 8
which gives a different result.
This may be completely new to people who have experience with other programming languages, and is one of the reasons why we recommend that you do all of the exercises at the end of this chapter. With a little practice, you will soon find this simple rule very natural, and you will consider it a relief that you do not have to remember complex rules for function precedence.
4.4. Monadic Scalar Functions#
Most of the symbols we have encountered so far also have a monadic definition; let’s look at them now.
4.4.1. The Four Basic Symbols#
We will begin with the four basic symbols +
-
×
÷
.
4.4.1.1. Conjugate#
The Plus sign used monadically is the function conjugate. It returns the complex conjugate of its arguments:
+ 0J1
When the numbers have no complex part, the function acts as the identity function, as the complex conjugate of a real number is the number itself:
+ 1 2 3 ¯3 6.5346 56J0
For historical compatibility reasons, monadic +
also acts as the identity function if the argument is a character:
+ 'a'
Because it is a scalar function, it naturally works with mixed arrays:
+ 'B' 3 'a' ¯3 0J1 'g' 4J¯4
4.4.1.2. Negate#
The minus sign is the function negate. It returns the negation of its argument:
- 19 11 ¯33 0 ¯17
4.4.1.3. Direction#
The multiply symbol used monadically is the function direction. For real numbers, it tells us the sign of its argument, using the following convention:
Result |
Value |
---|---|
|
The value is positive |
|
The value is zero |
|
The value is negative |
Some examples follow:
× 19 11 ¯33 0 ¯17
That is why the monadic multiply symbol is sometimes referred to as the sign function.
But direction also applies to complex numbers, and does so by extending the idea above. The direction of a number is that same number re-scaled so that it has magnitude (“length”) 1.
Let us take the complex number 3J4
as an example. If we look at the figure below, we see that the 3 is represented by the bottom side of the triangle and the 4 by the right side of the triangle. The little arrow that goes from the lower-left to the upper-right vertex of the triangle represents the direction in which 3J4
points.
We can also see that said arrow has length 5, as the Pythagorean theorem tells us:
((3*2)+(4*2))*0.5
Now ×3J4
is the complex number associated with a triangle that is similar to the one above, but with the arrow measuring 1, exactly like the smaller triangle inside the larger triangle in the figure below:
We only have to figure out the lengths of the other two sides:
× 3J4
4.4.1.4. Reciprocal#
No surprise here, as the symbol divide gives the reciprocal or inverse of its argument:
÷ 2 ¯4 .3 .25 ¯7
4.4.2. Other Scalar Monadic Functions#
4.4.2.1. Magnitude (Absolute Value)#
The monadic stile |
represents the absolute (unsigned) value of its argument, if its argument only contains real numbers:
V ← 29.2 49.3 ¯14.8 0 ¯37.2
|V
Like monadic ×
, monadic |
generalises nicely for complex numbers. Take the complex number 12J5
and its representation as a right triangle in the figure below.
|12J5
is the length that is missing in the figure. In this case, it is
|12J5
From a geometrical point of view it becomes clear that ×
and |
are related for any number v
: if we take the small triangle (×v
) and we multiply it by the length of the larger side (|v
) then we get the original triangle, which represents the original (complex) number:
v ← 0 1 ¯1 2.3 ¯4.6 0J1 3J¯4.2
v≡(×v)×|v
4.4.2.2. Exponential#
The expression *n
gives \(e^n\), where \(e\) is the base of the natural logarithm, approximately \(2.71828\):
*1
* 1 0 3 ¯1
4.4.2.3. Floor and Ceiling#
Floor (⌊
) rounds its argument down, while ceiling (⌈
) rounds its argument up, to the nearest smaller or larger integer value, respectively:
v ← 51.384 48.962 0 12.5 ¯73.27 ¯9.99
⌊v
⌈v
To round a value to the nearest integer a commonly used method is to add 0.5 and then take the floor, or alternatively, to subtract 0.5 and take the ceiling, as shown here:
⌊v+0.5
⌈v-0.5
The results are the same, except when the decimal part of a number is 0.5. For example, 12.5 above gets rounded to 13 or 12 depending on which method is used.
4.5. Left and Right Tacks#
4.5.1. Same#
APL also includes two primitive functions whose monadic name is equal: same. These two primitives are the right tack (⊢
) and the left tack (⊣
), obtained respectively with APL+\ and APL+Shift+\.
Their monadic name is “same” because they act as true identity functions, returning their argument completely unchanged:
⊢ 1 2 3
⊢ 'Banana'
⊢ 'Bag' 3 (1 2 'Bag') ('Bag' ('Bag' 3) (2 2⍴1 2 3 4))
Exactly the same thing happens (i.e. nothing happens) if you replace all the ⊢
above with ⊣
, for example:
⊣ 'Bag' 3 (1 2 'Bag') ('Bag' ('Bag' 3) (2 2⍴1 2 3 4))
These two functions may look useless, but they are not. For one, a neat little trick we can do with them is to display a value of a variable that we just assigned; for example, compare the two code cells below:
matrix ← 2 3⍴1 2 3 4 5 6 ⍝ Nothing gets displayed explicitly
⊢matrix← 2 3⍴1 2 3 4 5 6 ⍝ The value of matrix is displayed
⊣matrix← 2 3⍴1 2 3 4 5 6 ⍝ The value of matrix is displayed
These functions become even more useful combined with the power of operators or tacit programming (explained in detail in future chapters), or if we take a look at their dyadic usages (explained next).
4.5.2. Dyadic Usage#
The left and right tacks differ in their dyadic usage, of course, otherwise there would be no point in having two primitive functions that behaved in exactly the same manner.
When used dyadically, the left tack returns its left argument and the right tack returns its right argument. If you ever forget which dyadic tack returns what, just remember that each tack returns the argument to which it is pointing:
1 2 3 ⊣ 'Bananas'
1 2 3 ⊢ 'Bananas'
4.6. Processing Binary Data#
Remark
Binary values are most often produced by the comparison functions that we have already seen. However, the result of any function (such as addition or subtraction) which is composed only of 1s and 0s can be used as a binary (or Boolean) value, and may be used as an argument to any of the special primitive functions that apply to Boolean values.
Among the various ways of producing binary results, membership appears to be one of the most interesting tools.
4.6.1. Membership#
Membership tells whether the items of its left argument are present (
1
) or not (0
) in the right argument, regardless of their position in it;it accepts arguments of any shape or type; and
the result produced always has the same shape as the left argument.
Some examples will help you understand the function:
23 14 41 19 ∊ 17 88 19 50 51 52 23 40
This means that 23 and 19 appear somewhere in the rightmost vector, whereas 14 and 41 do not. The left argument has 4 items, and so has the result.
The function membership can operate on arguments of completely different shape. For example, it is possible to detect the presence of each item of a vector in a matrix, or vice versa.
In an earlier chapter we used a matrix containing the first six months of the year:
⊢monMat ← 6 8⍴'January FebruaryMarch April May June '
We can ask if certain letters are present in this matrix:
'December' ∊ monMat
The result shows that all letters of the word “December” appear in monMat
, except “D” and lowercase “m” (which should not be confused with the uppercase “M” of March and May).
In this case we used a vector left argument and a matrix right argument. Let’s try it the other way around. The following expression tells us which letters in the matrix monMat
appear in the word “Century”:
monMat ∊ 'Century'
As you might imagine, any comparison between numbers and letters gives zero:
1952 ∊ '1952'
'1952' ∊ 1952
Remember that '1952'
is a vector of four characters, none of which can be found in the number 1952.
We recommend that you do exercise 25 to further explore membership.
4.6.2. Binary Algebra#
Binary values can be processed using half a dozen specialised primitive functions, the main ones being and, or, xor, and not. Additional functions will be described in Section 4.19.
The function and is represented by the symbol ∧
(APL+0), as it is in mathematics. It returns the result 1 if the left and the right arguments are both equal to 1:
0 ∧ 0
0 ∧ 1
1 ∧ 0
1 ∧ 1
We can condense the four expressions above into a single one, given that ∧
is also a scalar function:
0 0 1 1 ∧ 0 1 0 1
The function or is represented by the symbol ∨
(APL+9), as it is in mathematics. It returns the result 1 if the left or right argument is equal to 1.
The four possible cases are shown in the following expression:
0 0 1 1 ∨ 0 1 0 1
Xor is an acronym for eXclusive Or. It returns the result 1 if one of the arguments is equal to 1, but not if both are equal to 1.
In automation, the same function is generally represented by a circled plus sign, like \(\oplus\).
APL does not need a different symbol for the function, because xor is the same as one of the comparison functions we have already met: ≠
0 0 1 1 ≠ 0 1 0 1
The last function is the monadic function not. Represented by the tilde ~
(APL+t), it converts 0 into 1 and 1 into 0:
~ 0 1 0 0 0 1 1
Remark
And, or, and xor are scalar dyadic functions;
Not is a scalar monadic function; and
Membership is a dyadic function, but it is not a scalar function.
All these functions can be applied to binary data of any shape. For example, let us see if any of those items of forecast
, which are greater than 350 thousand euros, have been exceeded by actual
sales:
⊢bin ← (forecast>350) ∧ (actual>forecast)
A side note: the parentheses around the rightmost expression (actual>forecast)
are not strictly needed. However, they do no harm either, so we have added them here to help you read the expression, since you may not yet be fully familiar with APL’s order of evaluation.
4.6.3. Without#
Given a vector X
and any array Y
, the expression X~Y
returns a vector equal to X
, but in which all items of Y
have been removed. The size and shape of Y
is immaterial, only the individual items of Y
are used.
This function is called without.
'This Winter is warm' ~ monMat
Notice how the right argument above (monMat
) is a matrix.
'Congratulations' ~ 'ceremony'
The uppercase “C” is preserved here because it is different from the lowercase “c”.
matrix
0 2 4 6 8 10 12 ~ matrix
Of course, it also works on numbers.
4.7. Processing Nested Arrays#
When working with nested arrays, it is important to recognise whether or not you are using a scalar function.
4.7.1. Scalar vs. Non-scalar Functions#
In Section 3, we set up a nested vector children
, which is composed only of numeric items:
)copy DISPLAY
children ← (6 2) (35 33 26 21) (7 7) 3 (19 14)
DISPLAY children
The application of scalar functions is straightforward.
For example, when we add 50 to children
, the value 50 is added to each of the items of children
. As these items are themselves scalars or vectors, adding 50 means adding 50 to each of their individual items. This process continues through all levels of nesting, ensuring that 50 gets added to all the individual items of children
. The result is therefore structurally identical to children
:
DISPLAY children + 50
One way of expressing this behaviour is to say that the scalar functions (both the dyadic and monadic ones) permeate down through the structure of nested arrays, until they reach the lowest-level items, and then apply themselves at this level. They are said to be pervasive functions.
Non-scalar functions, like membership, are not pervasive.
what ← 19 (6 2) 3 (33 26)
what ∊ children
The item (6 2)
of what
is also an item of children
, hence membership gives the answer 1, and the same is true for the value 3.
In contrast, 19 is only an item of the fifth item of children
, it is not an entire item of children
. Because a non-pervasive function processes each item as a whole, 19
is not the same as (19 14)
, so the answer is 0. The same goes for (33 26)
, which is only part of the second item of children
.
4.7.2. Be Careful With Shape/Type Compatibility#
It is easy to add a vector of five scalar items to children
, because each of the five scalars can be added to the corresponding item of children
:
children + 10 20 30 40 50
But if we try to add a vector of five sub-vectors to children
, we must ensure that the shape of each sub-vector is compatible with the shape of the corresponding item of children
, for example
children + (4 8) (5 7 4 9) (1 ¯1) (100 200 500) (14 51)
or
children + (4 8) 0 (1 ¯1) (100 200 500) ¯100
If there is any incompatibility, a LENGTH ERROR
is issued:
children + (1 2)(2 3)(3 4)(4 5)(5 6)
LENGTH ERROR: Mismatched left and right argument shapes
children+(1 2)(2 3)(3 4)(4 5)(5 6)
∧
All of the items of our vector could have been added to the corresponding items of children
except the second one. APL has detected and signalled this error.
You must also be careful if a nested or mixed array contains character data; it will not be possible to apply any arithmetic function to the array as a whole.
4.7.3. Tally#
We have seen the dyadic functions match ≡
and not match ≢
, and now we will see how we can use their monadic versions to work with (nested) arrays.
We mentioned tally briefly in the first chapter, which is the monadic use of ≢
. Tally does exactly what its name suggests: it counts the amount of items an array has along its first dimension.
For a vector, this corresponds to its length:
≢ 1 2 3 4 5 6
≢ 'ui'
A nested vector is still a vector:
(4 5) 'a' 'Bag' (35 'Cat' 42)
≢ (4 5) 'a' 'Bag' (35 'Cat' 42)
For higher dimensional arrays, tally returns the size of its first dimension:
≢ 3 3⍴9 8 7 6 5 4 3 2 1
≢ 2 5 27⍴0
For scalars, tally returns 1:
≢ 3
≢ '3'
In case you don’t think it makes sense to have the tally of a scalar return 1, consider the sequence below:
≢ 'abcd'
≢ 'abc'
≢ 'ab'
⍝≢ 'a' ⍝ what should this give??
≢ ''
Putting things this way, it sure looks reasonable that ≢ 'a'
give 1, right?
4.7.4. Depth#
Dyadic ≡
and ≢
are very closely related, but their monadic versions aren’t as much. The monadic function depth ≡
helps us work with nested arrays, in that it helps us count how many levels of nesting there are.
The result of ≡
is really simple to understand. First of all, the depth of a scalar is 0:
≡1
≡'a'
≡4525324
Then, the depth of an array is 1 larger than the depth of its deepest item, or sub-array. For example, a simple vector like 1 2 3 4
only has scalars as items, whose depths are 0, so the depth of the vector will be 1.
≡1 2 3 4
Now let us consider a nested vector composed of simple vectors like the one we just saw.
⊢nested ← (1 2 3 4) (3 5) (10 20 30)
The items of nested
are vectors of depth 1, so the depth of nested
should be 2:
≡nested
≡
has one more thing to it: if the nesting of the sub-arrays is not uniform then the result will be negative.
For example, 1 2 3 4
has depth 1 and 42
has depth 0, so a vector composed of these two sub-arrays has depth -2:
≡(1 2 3 4) 42
4.8. Reduce#
4.8.1. Presentation#
A few pages ago we calculated the costs of some purchased goods:
⊢costs ← price × qty
How much did we spend?
10.4 + 11.5 + 10.8 + 24 + 16.9
But writing things out like this is cumbersome and depends on us looking at the scalars in the costs
vector. What if the price
or the qty
changes?
Mathematicians are creative people who long ago devised the symbol \(\sum\), always with a pretty collection of indices above and below, that is used to indicate the sum of some numbers. This symbol makes it complex to understand and is difficult to type on a keyboard.
In APL, the operation is written like this:
+/ costs
Simple, isn’t it? This expression gives the total of all the items in the vector. You can read this as “plus reduction” of the variable costs
.
To gain a better understanding of the process:
When we write an expression such as
+/ 21 45 18 27 11
it works as if we had written
21 + 45 + 18 + 27 + 11
and we obtain the sum 122
.
In fact, it works as if we had “inserted” the symbol +
between the values. But then, if we write
×/ 21 45 18 27 11
it works as if we had written
21 × 45 × 18 × 27 × 11
so, we get the product 5051970
.
Similarly, if we write
⌈/ 21 45 18 27 11
it works as if we had written
21 ⌈ 45 ⌈ 18 ⌈ 27 ⌈ 11
so, we get the maximum 45
. And so on for other functions we put to the left of /
.
Exercise:
Try to evaluate the following expression in your head or with pen and paper: 23⌈ ⌈ ⌈/ 17.81 21.41 9.34 16.53
Don’t panic! Remember to evaluate it symbol by symbol, from right to left.
4.8.2. Definition#
Reduce, represented by the symbol /
, belongs to a special category of symbols called operators.
In most programming languages the word operator is used to describe operations like addition, subtraction, multiplication, and so on. In APL such operations are called functions; typical examples are +
, -
, ×
and ⍴
. The word operator has a separate meaning in APL.
In APL, a function works on an array or between two arrays to produce a result:
price × qty
Whereas an operator applies to one or two operands (its “arguments”) to produce what we call a derived function. That is, after we use the operator on its operands, we get a (derived) function which may then be used with an array, or between two arrays, to produce a result:
+/
This is the representation APL gives to the plus reduction, where the tree-like structure shows that +
is an operand to /
.
We may then use this derived function with an array in order to get a result:
⊢stock ← +/ qty
In the expression above, the symbol /
is the operator. It takes the function +
as its single operand (similar to how we give arguments to functions) and produces the derived function +/
. This derived function is then applied to qty
, giving a result which is assigned to stock
and displayed with ⊢
.
Please note that the argument to a monadic function is always to the right of the function, whereas the function applied to a monadic operator (its operand) is always to the left of the operator.
All of the APL primitive dyadic functions may be used as the operand to reduce; you can even apply a dyadic user-defined function. This generality makes reduce, and other operators, extremely powerful.
Dyalog APL provides a total of around 20 such powerful operators, listed in Section 14.4. It is also possible to write your own operators, just as it is possible to write your own functions.
4.8.3. Reduction of Binary Data#
Among the typical usages of reduce are ∧
and ∨
applied to binary data.
∧/ bin
gives the result1
if all the items ofbin
are equal to 1;∨/ bin
gives the result1
if at least one of the items ofbin
is equal to 1;+/ bin
tells us how many items ofbin
are equal to 1.
You can verify it on some small examples:
⊢bin ← 1 1 1 0 1 0 1
∧/ bin
∨/ bin
+/ bin
⊢allOnes ← 1∨bin
∧/ allOnes
Let us revisit a vector named contents
(from Section 3):
⊢contents ← 12 56 78 74 85 96 30 22 44 66 82 27
Now we will answer some questions about the values of the items of contents
.
Are all the values greater than 20?
∧/ contents > 20
The answer is no.
Is there at least one value smaller than 30?
∨/ contents < 30
The answer is yes.
How many values are smaller than 30?
+/ contents < 30
The answer is 3.
4.8.4. Reduction of Nested Arrays#
When you apply a reduction to a nested array, you must check that the items of the nested array are compatible (in shape and type) with the function that you intend to apply:
+/ (4 8) (1 4) 10 (9 5)
The expression above works because all 2-item vectors can be added together, and a single scalar (the 10) can be added to an array of any shape, because +
is a scalar function.
Notice, however, that in the expression below the 3-item vector cannot be added to the other 2-item vectors, so APL reports an error:
+/ (4 8) (1 4) (1 2 3) (9 5)
LENGTH ERROR: Mismatched left and right argument shapes
+/(4 8)(1 4)(1 2 3)(9 5)
∧
4.8.5. Reduction With Non-Commutative Functions#
Another thing to be careful about is the use of reduction with non-commutative functions, like -
or ÷
. Reducing an array by such a function yields results which may be counter-intuitive, but which may nevertheless be useful in a number of applications.
For example, remember that
-/ 45 9 11 2 5
is equivalent to
45 - 9 - 11 - 2 - 5
which, by APL’s order of evaluation is equivalent to:
45 - (9 - (11 - (2 - 5)))
If, instead, we use the traditional mathematical convention that interprets
as
we get the result 18.
This kind of “alternating series” can be useful for some calculations, although only rarely for business applications.
4.8.6. Application 1#
The employees of a company are divided into three hierarchical categories, denoted simply 1, 2 and 3. Two variables contain the salaries and the categories of these employees; we define them below and then use ⍴
to show some of their initial values:
⎕RL ← 73
salaries ← ?100⍴5000
10⍴salaries
⎕RL ← 73
categories ← ?100⍴3
10⍴categories
We can see the salaries of the first three employees are, respectively, 2121, 4778 and 4914 (of some currency) and their respective categories are 1, 2 and 2.
With what we learned in Section 4.8.3 we can also find out how many employees belong in the third category:
+/ categories = 3
Now the employees ask for an increase in their salaries. Each category of employee requests a different percentage increase, as shown in the following table:
Category |
Upgrade |
---|---|
1 |
8% |
2 |
5% |
3 |
2% |
How much is that going to cost the company?
Let us just create a variable containing the three rates shown above:
⊢rates ← 8 5 2 ÷ 100
The first employee is in category 1, so the rate that applies to this person is
rates[1]
More generally, the rates applied to all of the employees can easily be obtained with rates[categories]
:
10⍴rates[categories]
Having the rates, we only have to multiply them by the salaries to obtain the individual increases:
10⍴ salaries × rates[categories]
Finally, by adding them all together, we discover how much it will cost the company:
+/ salaries × rates[categories]
Note that:
the expression remains valid regardless of the number of employees or categories;
the result has been obtained without writing a program (no loops, no tests); and
this expression can be phrased in the simplest possible English, namely:
Sum the
salaries
multiplied byrates
according tocategories
.
This illustrates how the implementation of a solution in APL can be very close to the way that the solution would be expressed in everyday language. It also shows the advantage of not having to deal with trivial and “irrelevant” matters such as looping, memory allocation, declarations, etc. before a working solution can be developed.
4.8.7. Application 2#
Imagine now that we want to calculate the average of a set of values, for example the values contained in the variable contents
.
To do that, we must:
add all the values:
+/ contents
count how many values we have:
≢contents
divide one by the other:
(+/contents) ÷ (≢contents)
The result is 56.
Again, because of APL’s simple rule for the order of evaluation, the rightmost set of parentheses could be omitted.
4.9. Axis Specification#
4.9.1. Totals in an Array#
4.9.1.1. Processing Arrays#
We have seen the result of applying reduction to vectors, but what about matrices and higher rank arrays?
As an example, let us recall the array prod
. Its 3 dimensions represent respectively:
5 years;
2 assembly lines;
12 months.
⎕RL ← 73
⊢prod ← ?5 2 12⍴50
We can calculate totals along any of these 3 dimensions: years, lines and months.
We specify the dimension (or axis) between brackets after the reduce symbol:
+/[axis] prod
For example, suppose we want to calculate the total monthly production values over the 5 years. Years are represented by the 1st dimension of prod
, so we write:
+/[1] prod
We obtain a 2 by 12 matrix, giving the production of the 2 assembly lines, month by month. If we were to divide this matrix by 5, we would get the average production for each month, per assembly line.
Now, let us add up the production numbers of the two assembly lines. Lines are represented by the 2nd dimension of prod
, so we write:
+/[2] prod
We obtain a 5 by 12 matrix, with the total production of both assembly lines, month by month, in each of the 5 years.
And finally, let us calculate the annual production of each assembly line. Months are represented by the 3rd dimension of prod
, so we write:
+/[3] prod
The result is a 5 by 2 matrix, in which the columns contain the annual production of the two assembly lines in each of the five years.
We shall learn more about axis in the “Operators” chapter; let us first explore another simple use of this operator.
Suppose that we would like to multiply each of the rows (or columns) of a matrix by different values; we can use axis to specify whether we multiply row-wise or column-wise. First, here is a matrix:
⎕RL ← 73
⊢tam ← ?3 5⍴9
Let us multiply row-wise:
tam×[1]5 2 10
And now column-wise:
tam×[2]2 5 0 2 1
4.9.1.2. Axis Is Like an Operator#
The dimension specified within brackets is the axis along which the function is applied.
This produces a derived function, and for this reason, the pair of axis brackets is often called the axis operator.
The syntax for axis does not quite follow the general syntax for operators, but it shares all other properties with genuine operators. Axis takes a function as its left operand (the derived function +/
in the last example above), the dimension specification as its “right operand” (3 in the example), and produces a derived function, which is applied to prod
to calculate the annual sums.
Viewed as an operator, axis is therefore dyadic. It is, however, important to emphasise that its “right” operand is not prod
, it is the expression within the brackets. This is the first example of an operator that takes an array as an operand. We will find some more as we explore operators later on.
4.9.2. The Shape of the Result#
The dimensions of prod
are
⍴prod
and we can see that the dimensions of +/[1]prod
, +/[2]prod
and +/[3]prod
depend on this shape. In fact, when the axis is 1, the shape of the result is the original shape without the 1st item:
⍴+/[1]prod
When the axis is 2, the shape of the result is the original shape without the 2nd item:
⍴+/[2]prod
And when the axis is 3, the shape of the result is the original shape without the 3rd item:
⍴+/[3]prod
You can see that reduction of a 3D array gives a 2D array, in which the summed dimension has “disappeared”. This is the origin of the term “reduce”; it reduces the rank of the array.
This rule will help you predict the dimensions of the result of a reduction.
Rule
When reduce is applied along the nth dimension of an array, the shape of the result is the same shape of the array, but without its nth item.
The rank of the result is 1 less than the rank of the original array.
Whenever you want to calculate the sum along a particular dimension of an array, think of the dimensions in terms of concrete things: years, lines, months, etc. This should help you.
4.9.3. Special Notations#
By default, if no axis is specified, the reduction is applied along the last dimension of the array.
This means +/prod
and +/[3]prod
are the same,
(+/prod)≡(+/[3]prod)
much like +/forecast
and +/[2]forecast
are the same:
(+/forecast)≡(+/[2]forecast)
But it is also common to work along the first dimension of an array. For this reason, APL includes a special symbol for reduction along the first dimension: ⌿
(you can type it with APL+/).
This means +⌿prod
and +/[1]prod
are the same,
(+⌿prod)≡(+/[1]prod)
much like +⌿forecast
and +/[1]forecast
are the same:
(+⌿forecast)≡(+/[1]forecast)
Note
If one specifies an axis after the symbol /
or ⌿
, the function is applied along the specified axis, regardless of the symbol that is actually used.
We demonstrate this with two examples:
(+⌿[3]prod)≡(+/[3]prod)
(+⌿[1]forecast)≡(+/[1]forecast)
4.10. Our First Program#
The expression we wrote in Section 4.8.7 to calculate the average of a set of values is one that we may want to use time and time again. So let us store it as a program, or, to use the proper APL terminology, as a user-defined function.
There are many different ways to define functions, and these will be covered in detail in the “User-Defined Functions” chapter. For now we shall use the simplest, which is perfectly suitable for straightforward calculation functions like this one. Let’s type:
Average ← {(+/⍵)÷(≢⍵)}
Average
is the name of the function. It is followed by the definition, delimited by a pair of curly braces{
and}
;⍵
is a generic symbol that represents the array that will be passed as the right argument of the function; and⍺
is a generic symbol that represents the array that will be passed as the left argument of the function, if any.
The symbols ⍵
and ⍺
are obtained using APL+w and APL+a, respectively.
For more complex multi-line functions it is obviously more appropriate to use a text editor. However, this is beyond the scope of this chapter.
Once defined, this function may be invoked directly, just as if it were a built-in (primitive) function:
Average salaries
Average 12 74 56 23
The word Average
can now be used in any APL expression. We have enriched the vocabulary which can be used to process data in this workspace (provided that we save it).
Be patient: we shall see many other possibilities in the “User-Defined Functions” chapter.
4.11. Catenate#
Catenate is a dyadic function which joins two arrays together and it is represented by the comma (,
). The function is sometimes referred to as concatenate, and people with programming experience in other programming languages generally recognise that name better.
4.11.1. Catenating Vectors#
Catenate is easy to understand:
X ← 24 15 67 89
Y ← 11 33 75
X,Y
It is like joining two sentences together, so it is easy to remember which symbol to use. You can see that the length of the result (≢(X,Y)
) is equal to the sum of the lengths of the arguments ((≢X) + (≢Y)
):
((≢X)+(≢Y))=≢X,Y
Character strings are processed in the same way:
C ← 'Tell me'
D ← 'More'
C,D
Note that there is no space inserted between the contents of the two vectors, just like there was no number inserted between X
and Y
when we catenated them above. When we catenate a vector of 7 characters (like C
) with a vector of 4 characters (like D
), the result must have 11 characters.
When you catenate an empty vector to another vector, the result is the same as the original. Let us define an empty numeric vector v
:
⊢v ← 0⍴0
(We could have used ⍬
instead.)
Notice how the numeric vector X
remains unchanged when catenated with v
:
X,v
Similarly, catenating character strings with ''
does nothing:
C,'',D
4.11.2. Catenating Other Arrays#
It is possible to catenate two arrays if their shapes are compatible. The axis along which the catenation is to be performed must be specified, if it is different from the default.
Let us use three matrices A
, B
and C
:
⊢A ← 3 4 ⍴ 'A'
⊢B ← 2 4 ⍴ 'B'
⊢C ← 3 3 ⍴ 'C'
The possible catenations are:
the vertical catenation of
A
andB
; and
the horizontal catenation of
A
andC
.
It is not possible to catenate B
and C
because none of their dimensions are compatible.
To catenate vertically, we want the two matrices to be stacked on top of each other, creating a matrix with more rows, i.e. we want to create a matrix with a larger 1st dimension:
A,[1]B
The shape of the result is
⍴A,[1]B
and writing
B,[1]A
puts B
on top of A
instead.
Similarly, to stack matrices A
and C
horizontally we create a matrix where the 2nd dimension is larger:
A,[2]C
The shape of the result is
⍴A,[2]C
and writing
C,[2]A
puts C
to the left of A
instead.
In the same way as for reduce, the axis operator indicates which dimension will change during the operation, as we can see by inspecting the shapes of A
, B
, C
and the stacked matrices:
⍴A
⍴B
⍴A,[1]B
Using ,[1]
changes the 1st dimension. Using ,[2]
instead, we change the 2nd dimension:
⍴A
⍴C
⍴A,[2]C
The following nested array shows the four possible different catenations side by side:
(A,[1]B) (B,[1]A) (A,[2]C) (C,[2]A)
Rule
It is possible to catenate two arrays A
and B
along a given dimension provided that they have the same rank, and provided that all other dimensions have the same lengths.
The operation is written as A,[i]B
if we wish to catenate along the ith dimension.
It is also possible to catenate an array A
of rank \(n\) to another array B
of rank \(n-1\). The catenation must then be done along a dimension of A
such that its other dimensions are strictly identical to those of B
.
For example, it is possible to concatenate a vector to a matrix, provided that the vector has the same length as the corresponding dimension of the matrix:
A,[1]'JUMP'
A,[2]'TOP'
In the first example, 'JUMP'
has length 4 and the shape of A
is 3 4
, so we can see 'JUMP'
as having shape 1 4
and so we must catenate along [1]
. The shape of the result is
⍴A,[1]'JUMP'
and we see it was the 1st dimension that changed.
In the second example, 'TOP'
has length 3 and the shape of A
is 3 4
, so we can see 'TOP'
as having shape 3 1
and so we must catenate along [2]
. The shape of the result is
⍴A,[2]'TOP'
and we see it was the 2nd dimension that changed.
Example
We can add a row of totals to the bottom of a matrix Y
with an expression like Y,[1] (+/[1]Y)
:
forecast,[1] (+/[1]forecast)
The parentheses are for ease of interpretation; they are not necessary.
In a similar way, it is possible to concatenate a matrix to a 3D array.
Example
We would like to append to prod
(a 3D array) the production of a subcontractor, organised as an array of 5 years and 12 months.
⎕RL ← 73
⊢subcon ← ?5 12⍴20
The shape of subcon
is
⍴subcon
and the shape of prod
is
⍴prod
The two must be catenated along the 2nd dimension of prod
and the result will have the shape
⍴prod,[2]subcon
You see, it is as if subcon
had the length 1 along the catenation dimension (the missing one), i.e. it as if subcon
had shape 5 1 12
.
prod,[2]subcon
4.11.3. Catenating Scalars#
When a scalar is catenated to an array it is repeated as many times as necessary to match the length of the appropriate dimension of the array.
Here are two examples, using the matrix A
from before:
A,[1]'-'
A,[2]'*'
This property is very useful, because it saves us working out how many items are needed to match the corresponding dimension of the array.
We can also catenate two scalars. The result is of course a 2-item vector:
7,9
4.11.4. Special Cases and Notations#
By default, if no axis is specified, catenation works along the last dimension of the array(s).
So
A,C
is equivalent to
A,[2]C
(A,C)≡(A,[2]C)
APL also includes a special symbol that means “catenate along the first dimension”; this symbol is a comma topped by a minus sign: ⍪
. It can be obtained by APL+Shift+,.
So
A⍪B
is equivalent to
A,[1]B
(A⍪B)≡(A,[1]B)
Note
If an axis is specified, the operation is processed according to the axis specification, regardless of the symbol (,
or ⍪
) that is actually used.
This means A,[2]C
and A⍪[2]C
are both equivalent to A,C
and A,[1]B
and A⍪[1]B
are both equivalent to A⍪B
.
4.11.5. Dyalog Idioms and Idiomatic Phrases#
It is common to see catenate used in conjunction with reduce, as ,/
. Let us start by understanding what it does:
0 (1 2) (3 4 5) (6 7 8 9)
,/0 (1 2) (3 4 5) (6 7 8 9)
'a' 'bcd' 'efghi'
,/ 'a' 'bcd' 'efghi'
We can see above that reduce by catenate joins all items of a vector together. Using ,/
is so common that you will get used to looking at ,/
and reading it as join, instead of reduce by catenate. Like ,/
, there are many expressions which can be understood as an entity at first sight (with some practice!).
For someone who knows nothing of APL the expression above may take a bit of time to digest (even if they have an extensive knowledge of other programming languages), and they cannot readily appreciate that an APL programmer can understand it immediately, without having to read each of the symbols one by one.
This is not a paradox. For young children, reading the word “Daddy” is complex. It requires the comprehension of a sequence of letters one-by-one. I presume that you no longer do that, do you? You do not read the letters; you understand the word as a whole. This is exactly the same for the above idiomatic phrase.
These common combinations of primitives are usually termed as idioms or idiomatic phrases. We prefer the latter denomination to avoid any confusion with Dyalog APL’s idiom recognition. APLcart is a very comprehensive online collection of such idiomatic phrases, although not all entries in APLcart are idiomatic phrases. It is also noteworthy that there is no objective criteria to determine if a combination of primitives is an idiomatic phrase or not: it depends on your skill, experience, and even the context you are working on.
Some idiomatic phrases are so common and useful that Dyalog APL introduced a special idiom recognition feature that speeds up the processing of APL code for many popular phrases. Both RIDE and the Windows IDE even have the capability of colouring these differently. For Dyalog APL, an idiom is like a “shortcut primitive” that replaces a sequence of consecutive primitives. This replacement will allow for a speed up of the code in which it shows up.
For example, we have been using a short idiom fairly often: ≢⍴
(or ⍴⍴
in Ancient times) computes the rank of an array. Because ≢⍴
is recognised by Dyalog APL’s idiom recognition feature, the interpreter doesn’t need to process two instructions:
find shape of right argument;
return tally of elements of right argument.
Instead, it processes a single instruction:
find rank of right argument,
which can be computed by a faster method than the 2 separate steps above.
4.12. Replicate#
4.12.1. Basic Approach: Compress#
To extract scattered values from a vector, we can use indexing:
contents[5 6 11]
We can also use a new function named compress. It takes a Boolean vector as its left argument, and any array of appropriate shape as its right argument. The items of the right argument which match the 1s in the left argument are preserved, whereas those which match the 0s are removed. It acts like a mask or a filter:
0 1 1 0 / 42 15 79 66
1 0 1 0 0 0 0 1 1 / 'Drumstick'
This is extremely useful, because we can use compress to select items which match a given condition.
For example, let us extract from contents
the values which are greater than 80.
The Boolean vector for the left argument is obtained by contents>80
, and the selection is made by:
(contents>80) / contents
Of course, the same operation can be applied to any array of any dimension. For higher dimensional arrays, we can use the axis to specify along which axis to compress. For example, if we have a matrix of chemical formulas:
⊢chemistry ← 3 5⍴'H2SO4CaCO3Fe2O3'
1 0 1 /[1] chemistry
By using /[1]
we are compressing the 1st dimension of chemistry
, hence selecting two rows, corresponding to the two 1s in the vector on the left.
1 1 0 1 0 /[2] chemistry
If we use /[2]
we are compressing the 2nd dimension of chemistry
, hence selecting three columns. In this example columns 3 and 5 have been removed.
Compress is an excellent tool which allows you to:
extract some useful items from a variable; or
remove some unwanted items from a variable, which is the same thing.
Tip
Every time you obtain a Boolean vector, you should immediately think of two major things you can do with it: count or select.
For example, using contents
, we can produce a Boolean vector that shows which items are smaller than 50:
bin ← contents < 50
Then, we can:
count the items that are smaller than 50:
+/ bin
select (or extract) said items:
bin / contents
Hint
Programmers who are new to APL and who are familiar with indexing as the natural selection mechanism may be tempted to use the Boolean selection vector to create some indices, and then use the indices to select the desired items.
This works very well, for example:
ix ← bin / ⍳≢contents
contents[ix]
However, this is an unnecessary complication that wastes memory and processing time, compared to the straightforward selection shown above.
4.12.2. General Case: Replicate#
In fact, compress is just a special case of a more comprehensive function named replicate. Its left argument can be any vector of integer values, each of which produces the following result:
Signum of the left item |
Effect on the corresponding right item |
---|---|
|
item is replicated the number of times specified by the left item |
|
item is suppressed |
|
item is replaced by as many fill items as is indicated by the left item |
The concept of a fill item is new and will be discussed in full in the “Nested Arrays” chapter. For now, you only need to know that the fill item for a simple numeric array is 0 and the fill item for a simple character array is a blank space.
Here are some examples, using the same left argument applied to numeric and character vectors:
0 1 3 0 / 42 15 79 66
42 and 66 have been removed, as their corresponding left items were 0. 15 was replicated 1 time and 79 was replicated 3 times, as indicated by their corresponding left items.
0 1 3 0 / 'boat'
2 ¯3 1 0 / 42 15 79 66
For the example above, 15 has been replaced by 3 zeroes because the fill item for simple numeric arrays is a zero.
2 ¯3 1 0 / 'boat'
For this example, the letter “o” was replaced by 3 blank spaces because the fill item for simple numeric arrays is ” “.
4.12.3. Scalar Left Argument#
If the left argument of compress or replicate is a scalar, it applies to all the items of the right argument.
When the left argument is 1, all the items are retained:
v ← 'Phew'
1/v
When the left argument is a positive integer, all items are replicated that number of times:
3/v
When the left argument is 0, no item is replicated and we get an empty vector:
0/v
''≡0/v
As you can see above, when we use 0 as the left argument to replicate we get an empty vector. Because v
was a simple character vector, we get an empty character vector.
Finally, when the left argument is a negative integer, all items are replaced by the fill item as many times as the magnitude of the left argument indicates:
¯3/v
Visually, we can’t distinguish this case from 0/v
because blank spaces aren’t visible, but there are 3×4
of them:
≢¯3/v
Replacing v
with a simple numeric vector should help clear any doubts about what is happening:
¯3/1 2 3 4
4.12.4. Replicate with Axis#
Like reduce and catenate, replicate works along the last dimension of an array by default. However, it is possible for it to work on any dimension by using the axis. It is also possible to use ⌿
, which we have already seen, to work on the first dimension by default.
For example,
0 1 0 ⌿ chemistry
which is equivalent to
0 1 0 /[1] chemistry
Beware, the result obtained this way is not a vector, but a matrix having only one row:
⍴0 1 0⌿chemistry
You must not confuse reduce and replicate: even if the symbol used is the same, they are completely different operations:
reduce takes a function as its left operand; it is a monadic operator (e.g.
+/ contents
); whereasreplicate takes a vector as its left argument and an array as its right argument; it is a dyadic function (e.g.
vec/ contents
).
4.13. Position (Index Of)#
4.13.1. Discovery#
It is very often necessary to locate the positions of particular values in a list of items. To solve this, APL has a special function named index of, represented by the Greek letter iota (⍳
). This symbol can be obtained by Ctrl+i (the initial letter of iota). Let us see how it works:
vec ← 15 42 53 19 46 53 82 17 14 53 24
vec ⍳ 19 14 53 49 15
Above we asked for the positions of five values (19, 14, 53, 49 and 15) and naturally we obtain five answers:
the result tells us that 19, 14 and 15 appear in positions 4, 9 and 1 respectively;
the result also tells us that 53 appears in position 3. This is of course true, but it also appears in positions 6 and 10, which are not included in the result. This is a necessary restriction: if we had searched for five values and obtained seven results, it would not have been possible to say where each value appears. This is the reason why index of returns only the first occurrence of each value; and
(We shall see later that this is an advantage: if instead we need to find all the positions in which a value occurs, there is another function that we can use - see Section 4.14.1 below).
surprisingly, the result tells us that 49 appears in position 12, though
vec
has only 11 items! This is the way that index of indicates a missing value. We shall see that it is a great advantage, too.
The following rule explains how dyadic Iota works when the left argument is a vector:
Rule
In the expression r ← haystack ⍳ needles
we look for the needles
in the haystack
, and
haystack
can be a vector of any type: numeric, character, mixed, nested;needles
can be any array (any type, any shape, any rank);r
will have the same rank and shape asneedles
;the items of
r
contain the positions of the first occurrence of the corresponding items ofneedles
inhaystack
;items which do not appear in
haystack
give the result1+≢haystack
.
'ABC' ⍳ 57
A number cannot occur in a 3-item character vector, so the result is 4.
4 8 ⍳ '4 8'
Similarly, characters cannot appear in a 2-item numeric vector, so each character results in a 3.
alpha ← 'ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789'
alpha ⍳ chemistry
The two lower case letters in chemistry
give the answer 38 because alpha
has 37 items. Also notice how the shape of chemistry
and alpha ⍳ chemistry
is the same, like the rule above specified.
(⍴chemistry)≡(⍴alpha⍳chemistry)
We can also use nested vectors:
'Tee' (3 7) 'Golf' ⍳ 3 7 (3 7) 'Tee' 'Green'
The function index of is one of the most important primitive functions in APL. It is very flexible and it can be used in many situations, as shown in the following examples.
Warning
In the expression A⍳B
we search for B
in A
whereas in A∊B
we search for A
in B
. Do not be confused!
4.13.2. Application 3#
A car manufacturer decided that they will offer their customers a discount on the catalogue price. The country has been split into 100 geographical areas, and the discount rate will depend on the geographic area according to the following table:
Area |
Discount |
---|---|
17 |
9% |
50 |
8% |
59 |
6% |
84 |
5% |
89 |
4% |
Others |
2% |
which we save to two vectors:
area ← 17 50 59 84 89
discount ← 9 8 6 5 4 2
The first task is to calculate the discount rate to be claimed for a potential customer who lives in area d
; for example
d ← 84
Let us see if 84 is in the list of favoured areas:
area ⍳ d
We can see that 84 is the 4th item in the list.
Let us find the current discount rate for this index position:
discount[4]
This customer can claim a 5% discount.
We could simply write
discount[area⍳d]
Now, what if a customer lives in any other area, such as 75, 45 or 93?
The expression area⍳d
will return the result 6
for all these area codes, because these values are absent from area
. Then, discount[6]
will always find the rate 2%, as specified. Here we can see that it is an advantage that index of returns 1 + the number of items in the vector to be searched.
A Vector Solution
The importance of this approach to finding the discount rates is that it is vector-based. If publicity attracts crowds and therefore d
is no longer a scalar but a vector, the solution is still valid.
As an example, consider the following vector of area codes and the respective discount rates:
d ← 24 75 89 60 92 50 51 50 84 66 17 89
discount[area⍳d]
We have achieved all this without a function, neither a “loop” nor a “test”. And it works for any number of areas. Readers who know other programming languages will probably appreciate the simplicity of this approach.
4.13.2.1. Changing The Frame of Reference#
In reality, the expression that we have just written is an example of an algorithm for “changing the frame of reference”. Don’t panic, this term may seem esoteric, but the concept is simple: a list of area numbers (the initial set) is translated into a list of discount rates (the final set). The algorithm comprises only the function index of and indexing:
Algorithm
r ← finalSet[initialSet ⍳ values]
Let us imagine the initial set to be an alphabet composed of both lowercase and uppercase letters, and the final set to be composed of only uppercase letters, with a blank space in the middle:
alphLower ← 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ'
alphUpper ← 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUVWXYZ*'
Now, let us write a sentence; we will write it in French in order to show what happens with missing characters:
tale ← 'Le Petit Chaperon-Rouge a bouffé le Loup'
If we apply the algorithm seen above, the expression will convert the text from lower to upper case:
alphUpper[alphLower⍳tale]
As one might expect, the characters '-'
and 'é'
, which are absent from the initial alphabetic set, have been replaced by the '*'
, the “extra” character at the end of the final set. This works because once again the final set is one item longer than the initial set.
Once more, the logical steps needed to solve the problem are easily translated into a programming solution, and the programmer can thereby direct all his attention to solving the problem.
4.13.3. General Case#
The left argument to index of need not be a vector. In fact, in the expression haystack ⍳ needles
, haystack
can be an array of any rank, type and shape, as long as it is not a scalar. However, needles
must have a shape that is appropriate for the haystack
you use.
We will see what this means by using a 3D haystack
as an example:
⊢haystack ← 2 3 4⍴1 2 3 4 5 6 7 8 9 0
When index of is used, the first thing Dyalog APL does is look at its left argument and try to figure out what is the shape of the things contained in the haystack
. For that matter, we first inspect the shape of the haystack
⍴haystack
and interpret it as
“
haystack
contains 2 items of shape3 4
”
which means we can look for needles with shape 3 4
inside the haystack
. Therefore, for needles
to be seen as “containing items of shape 3 4
” its shape must also end in 3 4
. Whatever precedes the final 3 4
in the shape of needles
is what dictates the shape of the final result.
If the shape of needles
is too short for it to match the shape of the items the haystack
contains, we get a RANK ERROR
because needles
doesn’t have enough dimensions.
Similarly, if the shape of needles
is long enough but its trailing dimensions don’t match those of the left argument, we get a LENGTH ERROR
because the things we are comparing have different lengths along their dimensions.
In the table below we give examples of some haystack
and needles
shapes, along with the shape of the items that ⍳
thinks haystack
contains and the shape of the result r ← haystack ⍳ needles
(remember that ⍬
is the shape of a scalar):
|
|
|
|
---|---|---|---|
|
|
|
|
|
|
||
|
|
||
|
|
||
|
|
||
|
|
|
|
|
|
||
|
|
||
|
|
|
|
|
|
||
|
|
||
|
|
As a practical example, if we take the haystack
we defined above, we can look for 3 by 4 matrices in there. Our needles
variable has 4 such matrices:
⊢needles ← 4 3 4⍴1 2 3 4 5 6 7 8 9 0
haystack ⍳ needles
This shows that the first and second matrices were found in positions 1 and 2, respectively, and the third and fourth matrices were found nowhere, thus getting 3 as a result. The result is 3 because 2 is the number of items that haystack
has.
Now we write the rule that dictates how index of works in the general case:
Rule
In the expression r ← haystack ⍳ needles
we look for the needles
in the haystack
, and
haystack
can be any array of rankhr
withhr
being at least 1;needles
can be any array of ranknr
withnr
being at leasthr-1
;the last
hr-1
numbers of the shape ofneedles
and the lasthr-1
numbers of the shape ofhaystack
must be the same;r
has rank equal tonr-hr-1
and shape equal to the firstnr-hr-1
numbers of the shape ofneedles
;the items of
r
contain the positions of the first occurrence of the corresponding items ofneedles
inhaystack
;items which do not appear in
haystack
give the result1+m
ifm
is the leading number in the shape ofhaystack
, i.e.m ← ≢haystack
.
4.14. Where#
The primitive function where is the monadic use of the iota underbar ⍸
, which you can type with APL+Shift+I. The simplest use case for this primitive is to give it a simple Boolean array, for which ⍸
finds where the values 1 are.
For example, given the contents
vector:
contents
What items are greater than 75?
contents > 75
And where are they?
⍸contents > 75
They are at positions 3, 5, 6 and 11, referring respectively to the elements 78, 85, 96 and 82.
4.14.1. Application 4#
You probably remember that the function index of returns only the first occurrence of a value in a vector (cf. Section 4.13.1). Using where we can find all the occurrences.
Here is a vector, in which we would like to find the positions of the number 19:
vec ← 41 17 19 53 42 27 19 88 14 56 19 33
vec=19
Now that we have a Boolean vector, we can just use ⍸
to find out where the 1s are:
⍸vec=19
It is as simple as that!
Of course the same search technique will work on characters, because what we really care about is the Boolean vector we generate at a later step, not what the initial vector was. For example, let us find all the letters “a” in a sentence:
sentence ← 'Panama is a canal between Atlantic and Pacific'
⍸sentence='a'
Having found all the “a”s, we may wish to find all the lowercase vowels. For that matter, we need to create a Boolean vector with 1s in the positions with lowercase vowels. In the example above, sentence='a'
worked because we are allowed to compare a vector with a single scalar, but now we can’t change this to
sentence='aeiouy' ⍝ 'y' is a vowel in many European languages
LENGTH ERROR: Mismatched left and right argument shapes
sentence='aeiouy' ⍝ 'y' is a vowel in many European languages
∧
The code above does not work because =
is trying to compare the two vectors item by item, only to realise the vector 'aeiouy'
is too short for that. Instead, what we can do is check whether or not each character of sentence
is a member of the character vector 'aeiouy'
:
sentence∊'aeiouy'
Having computed this Boolean vector, the finishing touch is to compute where the vowels were found:
⍸sentence∊'aeiouy'
4.14.2. Increasing The Dimension#
Up until now we only used simple vectors as arguments to where, but in the beginning we talked about Boolean arrays, not vectors.
For higher dimensional arrays where behaves the same way: it returns the indices of the positions that contain 1s. Similar to what we did above, we might want to find all the lowercase vowels in the monMat
matrix:
monMat
⍸monMat∊'aeiouy'
The last item in the result above is 6 4
, which means that row 6, column 4 of monMat
contains a lowercase vowel: the final “e” in “June”. We can verify this with the function index⌷
you already learned about (cf. Section 3.5.6):
6⌷monMat
6 4⌷monMat
The example above also shows an interesting property of where: it always returns a vector, regardless of the shape of the input, and each item of the resulting vector is a suitable index to ⌷
.
4.14.3. Simple, Not Nested#
Where does require that its argument be a simple array. If you provide a nested array, ⍸
won’t know what to do with it. This is because there is no way of indexing into a nested array with an index vector like 3
or 6 4
, or even something longer.
In fact, if the argument to where is a nested array, you get a DOMAIN ERROR
:
⍸(0 1)(1 1)
DOMAIN ERROR: Numeric value required
⍸(0 1)(1 1)
∧
4.14.4. Beyond Boolean Arrays#
Now that you have seen the most iconic use of where, you will be shown the full specification for it, as we have only used Boolean arguments so far.
Rule
In the expression R ← ⍸Y
:
Y
must be a simple Boolean vector or a simple numeric array with non-negative integers;R
is a vector, regardless of the shape ofY
;each non-negative element of
Y
has its index repeated inR
as many times as the element’s value;if
Y
only contains zeroes,R
is an empty vector;each element of
R
can be used asi
ini⌷Y
to retrieve an element fromY
.
This repetition is similar to the way compress works when the left argument only contains non-negative integers:
3 0 0 2 0 1 / 'words!'
The left argument to compress tells it to use the 1st element 3 times, to use the 4th element 2 times and to use the 6th element 1 time.
The same argument to ⍸
returns the indices repeated as many times as they are needed:
⍸3 0 0 2 0 1
In this case, we could even achieve the same effect as the compress expression above by using []
indexing:
'words!'[⍸3 0 0 2 0 1]
This breaks down if we want to include negative integers in the left argument to compress, which where doesn’t support.
In fact, if the argument array contains negative integers or other numbers which aren’t integers, DOMAIN ERROR
s are issued:
⍸0 0 1 0 ¯1
DOMAIN ERROR: Where right argument must be non-negative
⍸0 0 1 0 ¯1
∧
⍸0 0 1 0 0.3
DOMAIN ERROR: Where right argument must be integer
⍸0 0 1 0 0.3
∧
4.14.5. Comparison of Membership, Index Of and Where#
We have discovered two different techniques, using the primitive functions membership and where, that allow us to look up one set of values in another and to determine the positions of the items of one set in the other. Depending on the problem that we have to solve, we can choose which of the two methods will be most appropriate for the job in hand. Consider the following example:
4.14.5.1. Example#
Imagine a company has subsidiaries in a number of countries; each country being identified by a numeric code. The country names are stored in a matrix named countries
, and the country codes are stored in a vector named codes
. To make things easier to read, let us show those two variables:
⊢countries ← 9 13⍴'France Great BritainItaly United StatesBelgium Swiss Sweden Canada Egypt '
⊢codes ← 50 43 12 83 64 34 66 81 37
Now let us show those two variables side by side:
countries, codes
So, Sweden is identified by 66 and Belgium is identified by 64.
All the sales made during the last month have been recorded in two vectors:
bhcodes
identifies in which country each sale has been made, andbhamounts
identifies the amount of each sale.
Here are the two vectors:
⊢bhcodes ← 83 66 12 83 43 66 50 81 12 83 12 66
⊢bhamounts ← 609 727 458 469 463 219 431 602 519 317 663 631
Some countries have not sold anything (Belgium, for example) whereas other countries made several sales (Italy, for example).
4.14.5.2. First Question#
We would like to focus on some selected countries (43, 50, 37 and 66) and calculate the total amount of their sales. Let’s first identify which items of bhcodes
are relevant:
selected ← 37 43 50 66
bhcodes ∊ selected
Then we can apply this filter to the amounts, and add them up:
(bhcodes ∊ selected) / bhamounts
+/ (bhcodes ∊ selected) / bhamounts
An alternative solution is to find the positions of the selected countries, then use this set of indices to get the amounts, and add them. The result is, of course, the same:
positions ← ⍸bhcodes ∊ selected
+/bhamounts[positions]
As mentioned previously in Section 4.12.1, it is kind of a detour to solve this task using indexing, but here it serves to illustrate the different lookup methods.
Let us take a look at the selected countries and their positions in bhcodes
:
selected
positions
Using membership, we have obtained positions
which contains 5 items for the 4 countries in selected
. This tells us that:
positions
contains the indices of all of the occurrences of the selected countries in the list of sales;however, the items in
positions
do not correspond to the items inselected
on a one-to-one basis; we cannot say that country 43 is in position 2, or country 50 in position 5, and so on; andit does not tell us that nothing was sold in country 37. Perhaps it would have been a good idea to identify this?
4.14.5.3. Second Question#
Now, let us suppose that we want to display the names of the selected countries. To do this, we must determine the positions of the selected country codes in the entire list of country codes, and get the corresponding names.
If we use the membership approach, here is what we get:
selected
positions ← ⍸codes ∊ selected
countries[positions;] , selected
(As a side note, above we catenated a character matrix with a numeric vector, so the result is a mixed matrix)
At first sight, this seems to be good: all the selected countries are displayed. However, they are not in the correct order: France is not 37, Sweden is not 50 and Egypt is not 66.
The problem with this method is the lack of a one-to-one correspondence between the selected countries and their positions in the list of sales. The positions will always be in the order that the countries appear in countries
- because of ⍸
. However, the Boolean vector codes ∊ selected
is completely independent of the order of the items in selected
: the expression returns the same result no matter how selected
is ordered.
The correct method to use in order to solve this task is to use the function index of (dyadic iota):
positions ← codes ⍳ selected
countries[positions;] , selected
It is the one-to-one relationship between the items of the right argument to index of (selected
) and the items of its result (positions
) that guarantees a correct result.
4.14.5.4. Comparison#
The following table summarises the most important properties of the two methods:
Method |
Properties |
---|---|
|
the items in |
instead, the items in |
|
|
|
|
|
|
the items in |
|
|
|
The choice of method depends on the kind of problem you want to solve.
4.15. Index Generator#
4.15.1. Basic Usage#
When used as a monadic function, the symbol iota generates a vector of the first N
positive integers. It is called index generator.
⍳9
If we have to extract the first 12 items of a vector contents
, we can write:
contents[1 2 3 4 5 6 7 8 9 10 11 12]
It is, of course, much easier to write
contents[⍳12]
The result can be combined with simple arithmetic operations. For example, suppose we need to produce the following list of 6 values: 115 122 129 136 143 150 (note the increments of 7). We can do this as follows:
⍳6
(⍳6)-1
7×(⍳6)-1
115+7×(⍳6)-1
More generally, any arithmetic series of integers can be produced by the following algorithm:
Algorithm
r ← origin + step × (⍳length) - 1
Special Case
If ⍳n
gives a vector of length n
, then ⍳0
should give a vector of length 0
, right? A vector having length 0 is an empty vector. In particular, this will be a numeric empty vector.
Let us check:
⍳0
Nothing gets displayed above, so it does look like the numeric empty vector ⍬
, which we can confirm with match:
⍬≡⍳0
This was useful in early versions of APL, before the introduction of the zilde symbol (⍬
).
The definition of ⍳n
given above reflects only a limited part of what this function can do; you will find more information in Section 4.19 at the end of this chapter.
4.15.2. Application 5#
Sometimes, a programmer needs to remove duplicate items from a vector and there is a well-known idiomatic way to do this. The idiom applies equally to numeric, character and nested vectors. Let us begin with a numeric vector:
vec ← 12 89 57 46 12 50 36 47 83 46 27 12
The algorithm is based on the comparison of two vectors:
⍳≢vec
which gives the position of each item of vec
, and
vec⍳vec
which identifies the first index in vec
in which each element of vec
is found. This may be a bit more complex to understand, so take your time to digest it. We are using ⍳
to identify the positions of the items of vec
in vec
itself. But because index of only returns the first occurrences, we get, for each item, the position where this value appears for the first time.
If we then compare these two vectors, we get a Boolean vector indicating which items of vec
are their first occurrences and which are repetitions:
(⍳≢vec)=vec⍳vec
Finally, we can use compress to keep only the first occurrences, so that the algorithm is as follows:
Algorithm
((⍳≢vector) = vector ⍳ vector) / vector
((⍳≢vec)=vec⍳vec)/vec
The result above has no duplicates.
It also works on character arrays and on nested arrays:
text ← 'All men are created equal'
((⍳≢text)=text⍳text)/text
⊢nested ← 'one' 1 (2 2⍴⍳4) 9 'nine' 'five' 'nine' 1 'two' (2 2⍴⍳4) 'two' 'one'
((⍳≢nested)=nested⍳nested)/nested
4.15.2.1. Unique#
Although still useful as an example, the idiom described above is now completely obsolete because there is a primitive function in APL that does exactly this.
The primitive, called unique, is represented by the symbol ∪
(APL+v) used as a monadic function:
∪ vec
∪ text
∪ nested
The symbol ∪
is the mathematical symbol for set union and we will see this is not a coincidence.
4.15.3. Application 6#
Some applications of index generator are extremely basic, but so useful! Suppose that you invest a certain sum of money, €6,000 for example, and you expect an interest rate of 4% p.a.. How is the investment expected to grow in the next 5 years?
We will have to calculate 1.04 to the power of 0, 1, 2, 3 and 4. The index generator will help us:
6000×1.04*(⍳6)-1
4.16. Ravel#
The function ravel is represented by the monadic use of comma (,
). Applied to any array, it returns all its items as a vector.
Naturally, if the array is already a vector, ravel does not change anything.
Let us see how it works on some matrices.
⎕RL ← 73
⊢tests ← ?6 3⍴100
,tests
The items of the matrix have been strung out and returned as a vector, row by row.
chemistry
,chemistry
A common use of ravel is to transform a scalar into a 1-item vector. The difference between a scalar and a 1-item vector is not readily obvious, until you use it as an index into a matrix.
Suppose that you need to select a particular set of columns cols
from the matrix forecast
. As long as cols
contains more than one value, the result will be a matrix:
cols ← 1 4 6
forecast[;cols]
But if cols
happens to have only a single value, which is a scalar, the result returned is a vector (we already mentioned this in Section 3.5.3):
cols ← 4
forecast[;cols]
The rank of the result may be critical if some other expression in your program expects a matrix.
To make certain that your indexing expression always returns a matrix, you must ensure that your index will always be a vector by using ravel:
forecast[;,cols]
With ,cols
, whatever the rank of cols
is, the result will always be a matrix.
Ravel can be associated with an axis specifier; this will be discussed in Section 4.19.5.
4.17. Empty Vectors and Black Holes#
When we apply a scalar dyadic function to a vector and a scalar, the rule is that the result has the same size as the vector:
42 75 86 31 + 10
Above, a scalar added to a 4-item vector gives a 4-item vector, too.
Below, a 7-item vector compared to a scalar gives a 7-item vector:
'MAMMOTH' = 'M'
But what happens if the vector is empty? The rule says that the result must have the same size as the vector: therefore it should be empty, too, regardless of the (scalar) function that we used!
hole ← ⍬
hole + 3
Notice how nothing is displayed above: the result is empty.
The same thing happens if we multiply by 100, for example:
hole × 100
hole = 0
hole = hole
⍴hole
hole ≡ ⍬
If you were working on the terminal and getting all these empty results, you could compare hole
to ⍬
with match and conclude hole
is an empty numeric vector.
Empty vectors look very much like black holes: they absorb everything (but only when used with scalar functions). This may lead to some unexpected results.
4.17.1. Not All Empty Vectors Are Created Equal#
Something that might throw off new APLers is the fact that there are infinitely many different empty vectors. The first two examples being ⍬
and ''
:
⍬
''
They look the same when displayed as results, but they are not the same:
⍬≡''
And this is just the tip of the iceberg. If you recall the rule for where (monadic ⍸
), we know that ⍸Y
returns an empty vector if Y
only contains 0s, but an empty vector of what?
If Y
contains positive integers, then the result of ⍸Y
will contain some indices that can be used to index into Y
and the shape of those indices depends on the shape of Y
:
⍸ 1 0 1 0
⍸ 2 2⍴1 0 1 0
So, in the first example the result is a vector with 2 integers, and in the second example the result is a vector with 2 vectors, each containing 2 items.
If we change one of the 1s to a 0, here is what we will be getting:
⍸ 1 0 0 0
⍸ 2 2⍴1 0 0 0
Now the first result is a vector with 1 integer and the second result is a vector with 1 vector of 2 items.
If we change the final 1 to a 0, we will be getting a vector with 0 integers (an empty numeric vector ⍬
) and a vector with 0 vectors of 2 items. For APL, these are not the same thing:
⍸ 0 0 0 0
⍬≡⍸ 0 0 0 0
⍸ 2 2⍴0 0 0 0
⍬≡⍸ 2 2⍴0 0 0 0 ⍝ different empty vectors
Tip
When you are dealing with empty vectors, you can use the incantation ⎕SE.Dyalog.Utils.repObj
to get a more useful representation.
To illustrate this incantation, we will see what it returns for the empty vectors we saw above:
⎕SE.Dyalog.Utils.repObj ''
⎕SE.Dyalog.Utils.repObj ⍬
⎕SE.Dyalog.Utils.repObj ⍸ 0 0 0 0
⎕SE.Dyalog.Utils.repObj ⍸ 2 2⍴0 0 0 0
What the code cell above tells us is that ⍸ 2 2⍴0 0 0 0
matches 0⍴⊂2⍴0
, which you can check by copying & pasting the result above, or using APL+z to type ⊂
:
(0⍴⊂2⍴0)≡(⍸ 2 2⍴0 0 0 0)
(You will learn about what ⊂
does in a future chapter, don’t worry.)
4.18. Exercises#
Warning
The following exercises are designed to train you, not the computer.
For this reason, we suggest that you try to answer them on a sheet of paper, not on your computer. When you are sure of your answer, you can test it on the computer.
Can you evaluate the following expressions?
3 × 2 + 6 ≠ 3 × 2
12 6 27 ⌊ 11 + ⍳3
4 5 6 ⌈ 4 + 2 5 9 > 1 6 8
7 ⌊ 25 6 17 - (2 × 3) + 9 3 5
((8 + 6) × 2 + 1) × 3 - 6 ÷ 3
(⍴4⌈5) + 4⌈5
1 ⊢ 2 ⊣ 3 ⊢ 4 ⊣ 5 ⊢ 6 ⊣ 7
1 ⊣ 2 ⊢ 3 ⊣ 4 ⊢ 5 ⊣ 6 ⊢ 7
1 2 3 ⊢ 4 5 6
Try to evaluate the following expressions. Be careful: they are not as simple as might first appear!
2 2+2 2
2+2 2+2
2+2,2+2
2,2+2,2
Given the vector a ← 8 2 7 5
, compare the results obtained from the following sets of expressions:
1+⍴a
⍴a+1
and also
1+⍳⍴a
⍳¯1+⍴a
⍳⍴a-1
Using your knowledge of the order of evaluation in APL, re-write the following expressions without using parentheses.
((⍳4)-1)⌈3
7⌊(⍳9)⌈3
1+((⍳5)=1 4 3 2 5)×5
Given a variable a
, find an expression which returns the answer 1 if a
is a scalar, and 0 if it is not.
Given two scalars a
and b
, write an expressions which gives 7 if a
is greater than or equal to b
and 3 if a
is smaller than b
.
Given two scalars a
and b
, find an expression which returns:
an empty vector, if
a
is zero, whatever the value ofb
;0, if
b
is zero, buta
is not;3, if neither
a
norb
are zero.
Unfortunately, your keyboard has been damaged and your ∧
and ∨
keys no longer work. Which other symbols could you use to replace them?
You can test your solutions on the vectors l
and r
:
l ← 0 0 1 1
r ← 0 1 0 1
Given these three vectors:
g ← 1 1 1 0 0 1
m ← 0 0 1 1 0 1
d ← 1 0 1 0 1 0
Evaluate the following expressions:
g∨d
~g∧d
~g∨~d
d∧~g
g∧m∨d
(~d)∧(~g)
(m⌈g)=(m⌊d)
(m⌊g)≠(m⌈d)
Evaluate the following expressions:
0 < 0 ≤ 0 = 0 ≥ 0 > 0
'sugar' ∊ 'salt'
11 ≠ '11'
'14' ⍳ '41'
Write an expression to compute the number of times the letter “e” appears in a character vector. You can test it with this vector:
text ← 'The silence of the sea'
How many expressions can you write to retrieve the last element of a vector? Can you write one that uses the operator reduce /
?
You can test your expressions on these vectors:
vec ← 1 2 3 4 5 6
vec ← (1 2) 3 (4 5) 6
vec ← 0 (5⍴0) (3 4⍴0) (1 2 3⍴0)
We have conducted some experiments on a variable z
:
2 ⍴ z
1 7
+⌿ z
20
z = 9
0
0
1
0
What is the value of z
?
We have conducted some experiments on a variable z
:
z = 0
0 1 0 0
1 0 0 1
+/[2] z
20 6
+/[1] z
8 7 6 5
What is the value of z
?
What are all the positions of the letter “e” in the character vector specified in exercise 11?
Given a vector vec
of any size and type (numeric or character or even nested), try to extract the items of vec
which are in the odd positions (the 1st, the 3rd, the 5th, …).
You can test your solution on these vectors:
vec ← 1 2 3 4 5 6 7 8 9
vec ← 'CROANGGARYANTLULLBAPTWIZOSNSSB'
vec ← (1 2) (2 3) (4 5 6) (6 9 42 1024)
How many numbers are there in the variable prod
used in this chapter? Try to answer with and without using ravel (monadic ,
).
How is it possible to remove all the values which do not fall between 20 (inclusive) and 30 (exclusive) from a given vector?
Notice how these two expressions look the same:
≢vec
4
⍴vec
4
Can you tell if/how they are different?
Compute the depth (≡
) of these arrays:
42
42 41 40 39
2 2⍴42 41 40 39
1 (2 3) (4 5)
(1 2) (3 4 5) (6 7 8 9)
1 (2 (3 (4 (5 (6 7)))))
(2 2⍴4) (2 2⍴⍳47) (3 6⍴⍳18)
In a vector, we would like to replace all the values that are smaller than 20 by 20, and replace all the values that are greater than 30 by 30. How can we do that? (In other words, how might we clip the values of a vector so that they are between 20 and 30, inclusive?)
The following 6 expressions cannot be executed, but instead generate error messages; can you say why?
3+(5-(6+2)×4
121÷(⍳4)-3
(¯X+5)*2
⍴4 5 6+2 3-1
⍳3-5
⍸1 0 2 ¯1
Write an APL expression which produces a vector of 17 numbers, the first being 23, with each subsequent number being equal to the preceding one plus 11.
In a shop, each product is identified by a code. You are given a list of the codes and the corresponding prices:
pcodes ← 56 66 19 37 44 20 18 23 68 70 82
prices ← 9 27 10 15 12 5 8 9 98 7 22
A customer gives you a list of items they intend to buy as vector of code/quantity pairs: code, quantity, code, quantity, and so on.
wannaBuy ← 37 1 70 20 19 2 82 5 23 10
Can you evaluate their bill?
Note that this cannot be done easily in a single (and readable) APL expression, and you will therefore need to write several expressions.
The correct total should be 375.
We have organised a lottery, and we have created five vectors:
⎕RL ← 73
tickets ← ?1000⍴999999
sold ← tickets[(800+?200)?1000]
ours ← sold[(?200)?≢sold]
winners ← tickets[100?1000]
prizes ← ?100⍴1000
These are the meanings of these vectors:
tickets
has the numbers of all the existing tickets;sold
has the numbers of the tickets which have been sold;ours
has the numbers of the tickets we bought ourselves;winners
has the numbers of the winning tickets, andprizes
has the prize amount associated with each respective number inwinners
vector.
Now, try to answer the following 4 questions:
What are the numbers of the unsold tickets?
Are there some winning tickets which have not been sold?
How many winning tickets do we have?
How much did we win?
Can you calculate all the divisors of a positive integer number n
?
Here are some results to check your answer:
|
Divisors |
---|---|
1 |
1 |
2 |
1 2 |
3 |
1 3 |
24 |
1 2 3 4 6 8 12 24 |
1337 |
1 7 191 1337 |
1234321 |
1 11 101 121 1111 10201 12221 112211 1234321 |
Solutions: the solutions can be found at the end of this chapter.
4.19. The Specialist’s Section#
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.
4.19.1. Division Control - ⎕DIV and Other Mathematical Subtleties#
4.19.1.1. Zero Divided by Zero#
In the beginning of this chapter we saw that 0÷0
returns 1:
0÷0
One might argue this is the case because any number divided by itself should give 1, like
3÷3
¯0.56÷¯0.56
1.2J0.6÷1.2J0.6
But 0÷0
is indeterminate from a mathematical standpoint, meaning 0÷0
by itself has no correct value. What follows from this is that carefully crafted contexts can make 0÷0
evaluate to whatever you please.
Let us explore that possibility briefly but without much mathematical rigour. Let us define an APL function that behaves like the mathematical function \(f(x) = 5x\), i.e. a function that takes a number and multiplies it by 5:
F ← {5×⍵}
And let us evaluate it at many different values:
⊢x ← ¯1+15÷⍨⍳30
⊢fx ← F x
And now let us study the ratio \(f(x)/x\) for those several values of x
:
fx÷x
If you look closely, the vector above has plenty of 5s and then a single 1 in the middle, corresponding to the 0÷0
division.
5≠fx÷x
Let us try to create values of x
that are tighter around 0 and perform the same division.
⊢x ← ¯0.1+100÷⍨⍳20
⊢fx ← F x
fx÷x
No matter how close to 0 we are, these ratios are always going to be returning 5. Because our function \(f(x)\) is well-behaved, in this context 0÷0
should return 5. Of course having to factor this in is an inconvenience for programming languages and most of them either
raise an error when one tries to divide zero by zero; or
return a previously-agreed-upon value (which is usually 0 or 1).
By default, Dyalog APL returns 1 when we compute 0÷0
as you saw above. This may sometimes be inappropriate. Suppose that we want to calculate the sales growth for 5 products, but the production of the 3rd product has not started yet, so its sales are currently zero:
before ← 20 31 0 120 63
after ← 22 27 0 149 59
The growth (in percent rounded) can be calculated like this:
⌊0.5+100× (after-before)÷before
It is rather surprising to see that we’ve got a 100% growth on a product that does not exist!
To help circumvent this sort of issues, a so-called system variable named ⎕DIV
is included in Dyalog APL to change the behaviour of division by zero.
By default, ⎕DIV
is 0, but can also be set to 1.
|
Behaviour |
---|---|
0 |
|
Any other number divided by 0 gives a |
|
1 |
|
Any other number divided by 0 gives 0 |
To change the value of the system variable ⎕DIV
you just assign to it as you would any other variable:
⎕DIV ← 1
0÷0
5÷0
⎕DIV ← 0
0÷0
5÷0
DOMAIN ERROR: Divide by zero
5÷0
∧
Because ⎕DIV
is a variable, it can be localised in the header of a function. Then the particular behaviour of division remains specific to that function and its sub functions.
Trying to change ⎕DIV
to a value other than 0 or 1 also gives a DOMAIN ERROR
:
⎕DIV ← 4
DOMAIN ERROR
⎕DIV←4
∧
4.19.1.2. Zero To The Power of Zero#
We also saw that 0*0
returns 1:
0*0
This is yet another convention that is in place just to make life easier for programmers, as 0*0
is indeterminate from a mathematical standpoint, similar to 0÷0
.
But please, do not take my word for it! Notice that any number to the power of 0 gives 1:
¯3 4J5 1.2 (○1) 1 2 3 * 0
Notice how 0 to the power of any positive number gives 0:
4J5 1.2 (○1) 1 2 3 *⍨ 0
So how do we settle this for 0*0
? If we look at it from the “a number to the power of 0” perspective, it should give 1. If we look at it from the “zero to the power of a number” perspective, it should give 0. So Dyalog APL just chose to go with the 1=0*0
convention, which is a fairly common one.
4.19.2. Derived Functions#
Let us sum the items of a vector:
+/ contents
In this expression, reduce takes plus as its operand.
This pair of symbols creates a new function, a so-called “derived function”. The derived function is monadic in this case, and it takes the variable contents
as its argument.
contents
is not the right argument of /
, it is the argument of the derived function +/
.
This is so true that +/
could be parenthesised:
(+/) contents
It can also be given a name:
sum ← +/
sum 10 20 30
All operators create derived functions, i.e. functions that are derived from the function(s) the operators accept as operand(s).
4.19.3. Nor & Nand#
APL boolean algebra also includes two useful functions:
nor (stands for not-or), represented in APL by
⍱
. You can type it with APL+Shift+9, which is “above”∨
;nand (stands for not-and), represented in APL by
⍲
. You can type it with APL+Shift+0, which is “above”∧
.
The nor and nand functions give the negation of or and and respectively, and that is why their symbols are respectively ∨
and ∧
with a ~
on top.
This means that
a⍱b
is equal to~a∨b
a⍲b
is equal to~a∧b
as shown below:
a ← 0 0 1 1
b ← 0 1 0 1
2 4⍴ (a∨b),(a⍱b)
2 4⍴ (a∧b),(a⍲b)
Warning
Both ∨
and ∧
are associative, but ⍱
and ⍲
are not!
This means that (a∨b)∨c
is equivalent to a∨(b∨c)
and thus can be written a∨b∨c
. Similarly for ∧
. but for nor and nand this is not the case:
a⍱(a⍱b)
(a⍱a)⍱b
Changing the order of evaluation changes the result, so ⍱
is not associative. Same goes for ⍲
.
Another consequence of this is that if bin
has more than 2 items, ⍱/bin
is not equivalent to ~∨/bin
, and similarly for ∧
.
⍱/b
~∨/b
This is probably the reason why those two primitives are rarely used in common business applications, although they are extremely useful in electronic automation, because they represent basic logical circuits.
4.19.4. Index Generator of Arrays#
We have given a limited definition of the index generator, here is a more general presentation:
Rule
In the expression r ← ⍳dim
:
dim
represents the shape of an array (the array itself need not actually exist); andr
is the set of indices of all items of that array.
The shape of r
is given by dim
and each item of r
is the index (or the set of indices) of its own items.
Some examples should make this easier to understand. Let us take several arrays and inspect their shape:
⍴contents
⍳⍴contents
contents[⍳⍴contents]
⍴monMat
⍳⍴monMat
monMat[⍳⍴monMat]
⍴prod
⍳⍴prod
prod[⍳⍴prod]
We can see that, more generally, an array is identical to itself indexed by Iota of its shape: data≡data[⍳⍴data]
4.19.5. Ravel With Axis#
Ravel can be used with an axis specifier, using the notation r←,[axis] data
.
In this expression, axis
can be:
an empty vector;
,[⍬] chemistry
a decimal scalar, adjacent to an axis of
data
; or
,[0.5] contents
a subset of the axes of
data
.
,[2 3] prod
4.19.5.1. Empty Axis#
If axis
is empty, the result is obtained by appending a new dimension of size 1 to the list of data
’s dimensions.
For example, if ⍴data
is 12
, then ⍴,[⍬] data
will be 12 1
:
contents
⍴contents
,[⍬] contents
⍴,[⍬] contents
Or, if ⍴data
is 3 5
, then ⍴,[⍬]
will be 3 5 1
:
chemistry
⍴chemistry
,[⍬] chemistry
⍴,[⍬] chemistry
4.19.6. Fractional Axis#
If axis
is a fractional value, it is mandatory that it is “adjacent” to an existing dimension of data
.
This means that for an array of rank 3, the axis must be a value between 0 and 4 (exclusive).
The result is derived from data
by inserting a new dimension of size 1 in the list of data
’s dimensions. The new dimension is inserted according to the value of axis
.
It means that if
⍴prod
then:
⍴,[0.5] prod
⍴,[1.5] prod
⍴,[2.5] prod
⍴,[3.5] prod
These conventions are totally consistent with those defined for some other functions like laminate or mix.
An example with a smaller array is also presented:
'PUB'
⍴'PUB'
,[1.5] 'PUB'
,[0.5] 'PUB'
⍴,[0.5] 'PUB'
Notice how the result of ,[0.5] 'PUB'
looks like that of 'PUB'
, but while the latter is a vector of length 3, the former is a matrix with 1 row and 3 columns.
The actual value of the axis specification is not used for anything other than determining where to insert the new dimension. This means that, for example, all expressions of the form ,[1.xxx]
will have the same effect as the ,[1.5]
that we used above:
,[1.5] 'PUB'
,[1.0123] 'PUB'
4.19.7. A List of Dimensions#
If axis
is a subset of the axes of data
, it is mandatory that they are contiguous and in ascending order. It means that for an array of rank 3, the list of axes can be [1 2]
, [2 3]
or [1 2 3]
, but neither [1 3]
nor [3 2]
, for example.
The result is obtained by unravelling data
so that the dimensions mentioned in axis
are merged into a single dimension.
For example, imagine that data
is an array of shape 5 2 4 7
:
data ← 5 2 4 7⍴0
We investigate the shapes of different ravels with lists of dimensions as axis specification:
⍴,[1 2] data ⍝ 10 is 5×2
⍴,[2 3] data ⍝ 8 is 2×4
⍴,[3 4] data ⍝ 28 is 4×7
⍴,[1 2 3] data ⍝ 40 is 5×2×4
4.19.8. Border Cases#
If axis
is reduced to a single value, the operation returns data
unchanged:
chemistry
,[1] chemistry
,[2] chemistry
If axis
contains the whole set of the axes of data
, the operation is equivalent to a simple ravel with no axis specification, and gives a vector of the items of data
:
,[1 2] chemistry
,chemistry
For these reasons, ravel with axes can only “collapse” dimensions of arrays of rank 2 or more.
4.20. Solutions#
The solutions we propose in the following pages 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 4.1
Copy each expression to the APL session and execute them. If you are surprised by an answer, analyse the expression step by step, starting from the right. For example, for the first expression:
3 × 2 + 6 ≠ 3 × 2
3 × 2
6 ≠ 6
2 + 0
3 × 2
And now the remainder of the expressions:
12 6 27 ⌊ 11 + ⍳3
4 5 6 ⌈ 4 + 2 5 9 > 1 6 8
7 ⌊ 25 6 17 - (2 × 3) + 9 3 5
((8 + 6) × 2 + 1) × 3 - 6 ÷ 3
(⍴4⌈5) + 4⌈5
1 ⊢ 2 ⊣ 3 ⊢ 4 ⊣ 5 ⊢ 6 ⊣ 7
1 ⊣ 2 ⊢ 3 ⊣ 4 ⊢ 5 ⊣ 6 ⊢ 7
1 2 3 ⊢ 4 5 6
Solution to Exercise 4.2
2 2+2 2
is the addition of two vectors of size 2:
2 2+2 2
2+2 2+2
is the addition of 2 2
with 2
, and then with 2
again:
2+2 2+2
Finally, although 2,2
gives the same array as writing 2 2
out, the fact that we are catenating means we have to evaluate the catenation also from right to left, making it happen after some of the additions have already been computed.
2+2,2+2
2,2+2,2
Solution to Exercise 4.3
a ← 8 2 7 5
Comparing 1+⍴a
and ⍴a+1
:
1+⍴a ⍝ find the shape of `a` and then add 1 to that
⍴a+1 ⍝ add 1 to all items of `a` and then find the shape of that
And now on to 1+⍳⍴a
, ⍳¯1+⍴a
and ⍳⍴a-1
. The easiest way to understand these expressions is to figure out the order in which things are happening and inspecting the intermediate steps.
1+⍳⍴a
⍳¯1+⍴a
⍳⍴a-1
Solution to Exercise 4.4
((⍳4)-1)⌈3
To remove parenthesis, what you do is locate the outermost pair of parenthesis (which is ((⍳4)-1)
), determine the function to which this pair of parenthesis is an argument (the ⌈
) and find the other argument to the same function (which is 3
).
Then you check if you can swap the two arguments and, if so, remove the outermost pair of parenthesis:
((⍳4)-1) ⌈ 3
3 ⌈ ((⍳4)-1)
3 ⌈ (⍳4) - 1
Now if you wish to remove the parenthesis from (⍳4)
we have to move it to the right of the subtraction, but -
is not commutative so we have to employ a little trick to make sure the expression still evaluates to the same result:
3 ⌈ (⍳4) - 1
3 ⌈ ¯1 + ⍳4
Please note that this sort of thing is not always possible!
7⌊(⍳9)⌈3
7⌊ (⍳9) ⌈ 3
7⌊ 3 ⌈ (⍳9)
7⌊3⌈⍳9
1+((⍳5)=1 4 3 2 5)×5
1+ ((⍳5)=1 4 3 2 5) × 5
1+ 5 × ((⍳5)=1 4 3 2 5)
1+ 5 × (⍳5) = 1 4 3 2 5
1+5× 1 4 3 2 5 = (⍳5)
1+5×1 4 3 2 5=⍳5
Solution to Exercise 4.5
There are a couple of ways to determine if a
is a scalar or not. For example, we might check if its rank is 0 with 0=≢⍴a
. We might also check if its shape matches the empty numeric vector with ⍬≡⍴a
.
Solution to Exercise 4.6
A possible way to look at this is by realising that the expression we are about to create is supposed to give two different values depending on the value of a≥b
. If a≥b
is 1 the expression needs to evaluate to 7 and if not, it needs to evaluate to 3.
What we can do is write an expression that “returns at least 3, with a bonus 4 whenever a≥b
”. From that perspective, we can do 3+(4×a≥b)
which is the same as 3+4×a≥b
.
Solution to Exercise 4.7
An example expression could be (a≠0)⍴3×b≠0
. Here’s how to get there:
We have 3 restrictions, the first one being about the shape of the result and the other two about the value itself. Reading the first restriction and factoring it with the other two, we see that we should return an empty vector whenever a
is zero and a numerical vector otherwise.
This means our expression will be of the form (a≠0)⍴(...)
. If a
is zero, then a≠0
returns 0 and the right expression won’t matter, because we get an empty vector. If a
is not zero, then a≠0
returns 1, and we leave the value of the right expression as is.
Now we only have to figure out what goes into (...)
, which should be an expression that evaluates to 0 if b
is zero and 3 if b
is not zero. We don’t need to care about enforcing a
to be different from zero because if a
is zero we will get an empty vector either way…
Thus this sub expression can be something like 3×b≠0
, which evaluates to 3
or 0
depending on the value of b
being zero or not.
Putting everything together we get (a≠0)⍴3×b≠0
.
Solution to Exercise 4.8
replace
∧
with×
or⌊
;replace
∨
with⌈
.
∧
returns 1 if both of the arguments are 1 and 0 in any other case. Rephrasing this, we get that ∧
returns 0 whenever there is a 0 present. From this, we see that both ×
and ⌊
are suitable replacements. ×
because whenever there is a 0, multiplication by 0 gives 0. A similar thing happens for ⌊
because we are only dealing with arguments that are 0 or 1.
∨
returns 1 whenever there is a 1 as argument, so we see that ⌈
is a suitable replacement.
l ← 0 0 1 1
r ← 0 1 0 1
l ∧ r
l × r
l ⌊ r
l ∨ r
l ⌈ r
Solution to Exercise 4.9
g ← 1 1 1 0 0 1
m ← 0 0 1 1 0 1
d ← 1 0 1 0 1 0
g∨d
~g∧d
~g∨~d
d∧~g
g∧m∨d
(~d)∧(~g)
(m⌈g)=(m⌊d)
(m⌊g)≠(m⌈d)
Solution to Exercise 4.10
0 < 0 ≤ 0 = 0 ≥ 0 > 0
'sugar' ∊ 'salt'
Above we check which letters of 'sugar'
also show up in 'salt'
.
11 ≠ '11'
Here we compare each character of '11'
with the number 11.
'14' ⍳ '41'
And here we find the position of each character of '41'
in '14'
.
Solution to Exercise 4.11
A sensible way to approach this problem is to first compare the string with the character 'e'
and then sum the 1s that show up:
text ← 'The silence of the sea'
+/ 'e'=text
Solution to Exercise 4.12
You may have come up with expressions that are similar to these ones or maybe completely different! We show some possibilities below.
We included different types of vectors so that you could be sure your solutions also work for all sorts of different vectors.
vec ← 1 2 3 4 5 6
⊢/ vec
vec ← (1 2) 3 (4 5) 6
vec[≢vec]
vec ← 0 (5⍴0) (3 4⍴0) (1 2 3⍴0)
((≢vec)=⍳≢vec)/vec
(⍴vec)⌷vec
Solution to Exercise 4.13
Notice how 2 ⍴ 7
gives the first 2 elements of z
, which are 1 and 7.
Then
z = 9
0
0
1
0
gives both the shape of z
(a column with 4 rows) and the third element which is 9. The missing clue gives that the elements of z
sum up to 20, but we already have 1, 7 and 9 which sum to 17, so the remaining element of z
is a 3:
⊢z ← 4 1⍴1 7 9 3
2 ⍴ z
+⌿ z
z = 9
Solution to Exercise 4.14
z = 0
0 1 0 0
1 0 0 1
gives the shape of z
and some of its values:
? 0 ? ?
0 ? ? 0
Now +/[1] z
gives the sums of the columns, from which we extract
8 0 ? 5
0 7 ? 0
Finally, +/[2] z
gives the sums of the rows, from which we get that the missing ?
are 7 and -1:
8 0 7 5
0 7 ¯1 0
⊢z ← 2 4⍴8 0 7 5 0 7 ¯1 0
z = 0
+/[2] z
+/[1] z
Solution to Exercise 4.15
Positions can be retrieved with where:
text ← 'The silence of the sea'
⍸'e'=text
Solution to Exercise 4.16
There are a couple of different ways to solve this problem. From an indexing perspective, we might want to generate all the odd numbers up to, and including, the length of the vector:
vec ← 1 2 3 4 5 6 7 8 9
vec[¯1+2×⍳⌈(≢vec)÷2]
This works by first figuring out how many indices we are going to need:
⌈(≢vec)÷2
Then generating 5 consecutive numbers and adjusting them so they become the odd numbers:
¯1+2×⍳5
Another possibility is to compress the odd positions:
vec ← 'CROANGGARYANTLULLBAPTWIZOSNSSB'
(2|⍳≢vec)/vec
Another really ingenious solution makes use of the fact that reshape reuses the right argument if needed, and so we can just compress by reshaping 1 0
to the correct length:
vec ← (1 2) (2 3) (4 5 6) (6 9 42 1024)
((≢vec)⍴1 0)/vec
Solution to Exercise 4.17
Ravel will take a 3D array and create a long vector with all its items, so we just have to count them:
≢,prod
Alternatively, we can use some mathematics and realise that there are as many items in prod
as its shape allows, where multiplying the dimensions together will yield the total. Think of it like computing the area of a rectangle for a 2D array and the volume of a rectangular prism for a 3D array:
⍴prod
×/⍴prod
Solution to Exercise 4.18
This is a simple exercise in compressing with a Boolean mask. We just create a mask with all the numbers we want to keep:
⊢ns ← 10+⍳30 ⍝ just a vector to exemplify with
((ns<30)∧ns≥20)/ns
Be careful with using the correct Boolean operator. Swapping them around might create an expression which preserves the vector as-is:
((ns<30)∨ns≥20)/ns ⍝ swapped ∧ with ∨
Solution to Exercise 4.19
≢vec
⍴vec
The two expressions have the same value but they have different shapes. While ≢vec
is a scalar:
⍴≢vec
⍴vec
is a vector of length 1:
⍴⍴vec
Solution to Exercise 4.20
For this exercise we just have to revisit the rule for how depth works and work our way inside out.
A scalar has depth 0
≡42
and a simple vector has depth 1
≡42 41 40 39
because all its items are scalars of depth 0. Similarly for a simple matrix. Even though its rank is larger than the rank of a vector, the matrix only contains scalars of depth 0 and thus its depth is also 1:
≡2 2⍴42 41 40 39
For uneven nested vectors we just compute the largest depth, add 1 and then return it as a negative number. Since 1 (2 3) (4 5)
contains scalars (of depth 0) and vectors (of depth 1) its depth should be -2:
≡1 (2 3) (4 5)
On the other hand, (1 2) (3 4 5) (6 7 8 9)
is a vector of vectors, and thus its depth is 2:
≡(1 2) (3 4 5) (6 7 8 9)
Computing the depth of 1 (2 (3 (4 (5 (6 7)))))
is just a matter of counting carefully. The inner (6 7)
has depth 1, 5 (6 7)
has depth -2, 4 (5 (6 7))
has depth -3, …, up until
≡1 (2 (3 (4 (5 (6 7)))))
The final example is to try and throw you off by including items that have higher ranks, but still the same depth. A simple matrix has depth 1, so a vector of simple matrices is going to have depth 2:
≡(2 2⍴4) (2 2⍴⍳47) (3 6⍴⍳18)
Solution to Exercise 4.21
This task of ensuring all numbers in an array are above a minimum threshold and below a maximum threshold is sometimes called clipping and is easy to achieve in APL with the scalar ⌈
and ⌊
functions. We create a dummy matrix to demonstrate:
⎕RL ← 73
⊢m ← 15+?3 3⍴20
We want m
’s values to be at least 20, so numbers below should become 20:
20⌈m
Similarly for numbers above 30:
30⌊20⌈m
Solution to Exercise 4.22
The challenge of this exercise was to spot the error before trying to run the expression on the interpreter and reading its error message.
3+(5-(6+2)×4
Looking close enough, we see there is an extra (
that never got closed, getting us a SYNTAX ERROR
.
3+(5-(6+2)×4
SYNTAX ERROR: Unpaired parenthesis
3+(5-(6+2)×4
∧
121÷(⍳4)-3
This one won’t work because there is a hidden division by 0 in there:
(⍳4)-3
121÷(⍳4)-3
DOMAIN ERROR: Divide by zero
121÷(⍳4)-3
∧
(¯X+5)*2
The error in this expression is the usage of ¯
with a variable name. ¯
can only be used when writing out numbers. If you want to negate the value of a variable, use the function negate -
.
For us to show the appropriate error, we need to define X
to have some value.
X ← 42
(¯X+5)*2
SYNTAX ERROR
(¯ X+5)*2
∧
The “Invalid token” mentioned is the ¯
next to a variable name, when it should be next to a number literal.
⍴4 5 6+2 3-1
The problem with this expression is that we are using scalar functions with vectors whose lengths aren’t compatible. The 2 3-1
works because -
knows how to subtract a number from a vector, but then 4 5 6+1 2
will attempt to add two vectors of incompatible lengths and fail:
⍴4 5 6+2 3-1
LENGTH ERROR: Mismatched left and right argument shapes
⍴4 5 6+2 3-1
∧
⍳3-5
Index generator expects a vector of non-negative numbers as argument and so a simple DOMAIN ERROR
is issued:
⍳3-5
DOMAIN ERROR
⍳3-5
∧
⍸1 0 2 ¯1
Similarly, where only expects non-negative integers in its argument array, so the ¯1
is an issue there:
⍸1 0 2 ¯1
DOMAIN ERROR: Where right argument must be non-negative
⍸1 0 2 ¯1
∧
Solution to Exercise 4.23
This “vector of 17 numbers, the first being 23, with each subsequent number being equal to the preceding one plus 11” is called an arithmetic progression in mathematics. Generating them is really easy if you use the algorithm we showed earlier.
We start by generating a vector of consecutive numbers with the appropriate length:
⍳17
Then we multiply the whole sequence by the spacing we want there to be between each number:
11×⍳17
Then we add/subtract a number to the sequence so that the first element matches the required value. The first number is currently 11 and we want it to be 23, so we have to add 12:
12+11×⍳17
Notice the final expression is equivalent to the algorithm presented in Section 4.15.1, and working our way up to the expression is easier than trying to memorise an algorithm!
23+11×(⍳17)-1
Solution to Exercise 4.24
The first sensible thing to do is separate the products from the quantities. We can do this by creating a matrix with 2 columns:
wannaBuy ← 37 1 70 20 19 2 82 5 23 10
⊢m ← ((0.5×≢wannaBuy),2)⍴wannaBuy
Now the first column contains the codes and the second column contains the quantities. We chose 2 columns instead of 2 rows because of the way ⍴
fills in the matrix: from left to right, top to bottom.
The next thing to do is map the product codes to their prices:
pcodes ← 56 66 19 37 44 20 18 23 68 70 82
prices ← 9 27 10 15 12 5 8 9 98 7 22
⊢pos ← pcodes⍳m[;1]
Now that we have the positions of the products the customer wants, we can find the prices, multiply them by the quantities and sum everything:
+/prices[pos]×m[;2]
Solution to Exercise 4.25
⎕RL ← 73
tickets ← ?1000⍴999999
sold ← tickets[(800+?200)?1000]
ours ← sold[(?200)?≢sold]
winners ← tickets[100?1000]
prizes ← ?100⍴1000
The unsold tickets are the tickets
without the ones that have been sold
:
tickets ~ sold
Similarly, we can find the winners
’s tickets without the ones that have been sold
:
winners ~ sold
So we can see some of the winning tickets were not sold.
We have
≢ours
tickets but not all of them are winning tickets. However, we can easily see which of our tickets are winning tickets by checking which ones are members of the vector with the winning tickets:
ours∊winners
If we want to know how many tickets are winning tickets, all we have to do is sum up the 1s in the vector above.
+/ours∊winners
Finally, to find our earnings we need to connect ours
to the prizes
. The easiest way to do so is by using compress after finding out which winners
are also ours
:
+/ (winners∊ours)/prizes
Another (more convoluted) possibility would be to index into the prizes
with the winners
that were also ours
. For example, finding the positions of ours
in the winners
:
pos ← winners ⍳ ours
Now notice we cannot index immediately into prizes
:
prizes[pos]
INDEX ERROR
prizes[pos]
∧
This is because some of our tickets are not winners and hence their index is one too large. To circumvent this we can extend prizes
to have a trailing 0, so that our tickets that are not winners will contribute with 0 to the final sum:
prizes ← prizes, 0
+/ prizes[pos]
Solution to Exercise 4.26
Computing divisors of a number n
amounts to finding the numbers m
for which m|n
is 0. First we compute the vector of all candidates:
n ← 24
⊢m ← ⍳n
Now we compute the residues with, you guessed it, residue:
⊢r ← m|n
From these, we want the 0s because those represent numbers in m
that divide evenly into m
. Then we can use ⍸
to convert them back to the original integers:
⍸0=r