Ivy: An APL-Like Calculator

Ivy is an interpreter for an APL-like language. Described as a plaything, but actually very capable.

APL
Ivy
Author

John Bates

Published

November 22, 2023

Six months ago I wrote about my experience with J, another APL-like language. I had invested a lot of time learning a new language, and one that was very different to anything that I had used before. Ivy is a much simpler language than J, and I was keen to see whether my experience with J made it easier to pick up and also to see just how capable this small language was.

Note

The source code for Ivy is available on GitHub (here) and documentation can be found here.

If you do start to experiment with Ivy I recommend Ivy-Prompt - a line editor wrapper for Ivy with tab-completion and input history.

Ivy was written by Rob Pike and, I think, named after Ken Iverson who developed APL in the 1950s and 1960s. Unlike APL which uses a special character set, or J in which the operators of the language consist of combinations of punctuation characters, the input to Ivy is ASCII and the operators have short alphabetic names.

Ivy also has exact rational arithmetic supporting integers with up to 10000 digits and for irrational numbers, or integers with more than 10000 digits, high-precision floating point using 256 bits of mantissa. In fact, Ivy supports arbitrary precision arithmetic and these default values really just control how many digits of a result Ivy is prepared to print to the display before resorting to the more compact floating point format.

If you are used to working with calculators it comes as a bit of a shock that a sum like:

(3/8) / (5/9)

results in the value 27/40.

If you want a floating point result you need to force the overall result to have a floating point representation:

float (3/8) / (5/9)

which gives 0.675.

In addition to user-defined operators, Ivy has 53 unary and type-converting operators, 53 binary operators, and 6 higher level operators (adverbs in J terminology) that can be applied to existing operators to modify their behaviour.

In common with J, operators all have the same precedence, and expressions are evaluated in right-associative order. So, unary operators apply to everything to their right, and binary operators apply to the operand immediately to their left and to everything to their right.

I was keen to see how Ivy would cope with my simple list manipulation task that I had performed in my lure of J notebook. That task involved the following steps - their implementation in Ivy is given below:

  1. Write a function, outArray that, given a string title and a vector of integers as its two parameters, outputs that vector as a comma-separated list, preceded by the title, a count of the number of integers in square-brackets and a colon. So, called with the parameters "Initial" and 1 2 3 4, it would return:

    Initial [4]: 1, 2, 3, 4

  2. Assign a small vector of ints, say, the vector: 31 5 7 6 3 2 8 to the variable ints.

  3. Use the outArray function to output the ints vector with the title “Initial”.

  4. Create a new vector, sortedInts, from the original ints which contains the elements but sorted into ascending order, and output it with the outArray function using the title “Sorted”.

  5. Create a list, oddInts, that contains just those ints from sortedInts that are odd and output it with the title “Odd”.

  6. Create a list, argvInts, that has been read from the command line and output it with the title “argvInts”.

  7. Create a list, fileInts, that has been read from a file on the filesystem that contains a single line of comma-separated integers and output it with the title “FileInts”.

  8. Combine the oddInts, argvInts, and fileInts arrays into a single list, combinedInts, without duplicates and output it with the title “CombinedInts”.

  9. Calculate the average of the numbers in the combinedInts array and store it in the scalar average.

  10. Split the numbers in the combinedInts array into two distinct arrays, higher and lower such that higher contains those ints that are higher than the value in average and lower contains the remaining ints. Output both higher and lower with `outArray’.

The OutArray Function

I chose not to attempt a pure J to Ivy conversion for two reasons. Firstly, I wanted to explore what tools were available in the Ivy language and, secondly, I feared that my rather naive attempts at writing J functions would be exposed and I’d feel the need to re-write the J notebook.

A user-defined function to combine two integers with a comma and a space is useful as a helper function.

# Join a and b with a comma and space.
op a comma b = (text a), ', ' ,(text b)

In Ivy this declares a user-defined operator with the name comma that takes two arguments: a as its left-hand argument and b as its right-hand argument. The expression to right of the equals sign defines the body of the operator - in this case a simple expression.

The binary comma operator will combine its arguments into a single vector.

Elements of a vector are normally printed with a separating space between each element. However, if all of the elements in a vector are characters then, as a special case, they are printed without spaces between them.

To ensure that all of the elements in the vector returned by the comma operator are characters we apply the text operator to the ints a and b.

The same trick is performed in the body of the outArray operator to prevent Ivy from inserting unwanted spaces.

# Output title, count of ints and comma-separated ints.
op title outArray ints =
    title, "[", (text rho ints), "]: ", comma/ ints

The arguments to outArray are title on the left-hand side and ints on the right-hand size. Within the body of the operator definition these are treated as local variables.

rho ints

will give us the length of the ints vector. Applying text to the result will ensure that the overall value is a string.

text rho ints

The final part of the line comma/ ints is using the Ivy Reduce operator (/). Reduce modifies the operator that precedes it so that it applies the operator between each of its arguments. The easiest way to see this is by application to the plus operator. In Ivy +/ 1 2 3 4 is the same as 1+2+3+4. Similarly ’*/ 1 2 3 4’ is the same as 1*2*3*4.

Care needs to be taken to remember that an Ivy expression is parsed from right to left. So -/ 1 2 3 4 is equivalent to 1-2-3-4 which is parsed as 1-(2-(3-4)) and so has the value -2 rather than -8 which would be the value in a language where the expression was parsed from left to right.

Assignment and Output

We can assign a vector of integers to the variable ints like this:

ints = 31 5 7 6 3 2 8

And we can use our outArray operator to display their value:

'Initial' outArray ints
Initial[7]: 31, 5, 7, 6, 3, 2, 8

Sort the List

A vector can be sorted using Ivy’s up (or down) operators. Up is a unary operator which when applied to a vector B, say, returns a vector of the same length as B that contains that ordering of indices which, if used to access the elements of B, would return them in ascending order.

We can apply up to our ints vector:

up ints
6 5 2 4 3 7 1

So, to return the elements of ints in ascending order we would need to take the 6th element of ints followed by the 5th, followed by the 2nd and so on.

Indexing of a vector is performed using the [] notation. Index numbering begins at 1 in Ivy. So the first element of vector B is B[1].

A vector can also be indexed using another vector and this results in a new vector. For example, if B is indexed by the vector 3 1 2 we would obtain a new vector whose elements were made from the third, first and second elements of B, in that order.

So, indexing the ints vector by the result of applying up will give us the sorted values from ints:

sortedInts = ints[up ints]
'Sorted' outArray sortedInts
Sorted[7]: 2, 3, 5, 6, 7, 8, 31

Select the Odd Integers

We can select the odd integers from our vector, and assign them to the variable oddInts, with this:

oddInts = (sortedInts mod 2) sel sortedInts

This makes use of two binary operators: mod and sel. The use of round brackets reminds us that Ivy evaluates a line of input from right to left and so we need the brackets to obtain the desired result. Without the brackets ivy would parse the line as:

oddInts = sortedInts mod (2 sel sortedInts) # wrong

The operator mod when used in A mod B returns the remainder on dividing A by B. Any integer divided by 2 will have a remainder of 0 if it is even and a remainder of 1 if it is odd.

Ivy, being an array processing language, operates on entire vectors and so it makes sense to pass a vector as the left-hand operator of mod the resulting vector being the element-wise application of mod 2. You can think of the right-hand argument 2 as being widened to be a vector of the same length as the left-hand argument but consisting of all 2s.

So, for each element of the vector sortedInts that is odd we will see a 1 in the corresponding position in the result and for each element of sortedInts that is even we will see a 0 in the corresponding position.

sortedInts
2 3 5 6 7 8 31
sortedInts mod 2
0 1 1 0 1 0 1

The sel operator allows us to create a new vector by selecting 0 or more items from each position in a vector. In its simplest form it is used like this:

A sel B

Where A is a vector of the same length as B and contains either the value 0 or the value 1 in each position. Then for each position in A that contains a 1 the value from the corresponding position in B is used in the resulting vector and for each position in A that contains a 0 the value from the corresponding position in B is ignored.

So, if sortedInts contains 7 elements, the expression 1 1 1 0 0 0 0 sel sortedInts would return a vector of length 3 containing the first three elements of sortedInts.

sortedInts
2 3 5 6 7 8 31
1 1 1 0 0 0 0 sel sortedInts
2 3 5

As we saw above, our application of mod 2 resulted in a vector of the same length as sortedInts containing just 0s and 1s and with 1s in the positions that corresponded to odd numbers. So, combining the two and printing the results gives us our vector of odd integers.

oddInts = (sortedInts mod 2) sel sortedInts
'Odd' outArray oddInts
Odd[4]: 3, 5, 7, 31

Reading from the Environment and from a File

Ivy does not have the rich library environment that other APL-like languages have. Nor does it have an each operator that might make it easier to produce an operator that could read CSV formatted strings. So (currently) it would not be possible to reproduce steps six and seven of our task to read CSV input from the environment or filesystem. However, it does have a mechanism in which Ivy statements can be read from a file on disk and so we will read argvInts and fileInts using that method.

To read statements from a disk and continue execution we use the )get command, passing it the name of the file containing the ivy statements that we want to read.

So, if the file argvAndFileInts.ivy contains the following two lines:

argvInts = 33 44 55 66
fileInts = 5 8 10 12

We can read these values in from disk and print them out.

)get "argvAndFileInts.ivy"
'ArgvInts' outArray argvInts
ArgvInts[4]: 33, 44, 55, 66
'FileInts' outArray fileInts
FileInts[4]: 5, 8, 10, 12

Combine without Duplicates

We now need to combine the three vectors oddInts, argvInts and fileInts into a single vector of integers without retaining duplicates. This is made easy for us in Ivy with the unique operator.

combinedInts = unique oddInts, argvInts, fileInts

The unique operator is a unary operator that, when applied to an argument B, will return a new vector that is made by removing all of the duplicate elements from B.

There is a second operator in use in the above assignment - the binary comma operator (,). The comma operator, when applied between vectors A and B, will create a new vector in which the elements of B are appended to the elements of A creating a new vector whose length is the sum of the lengths of A and B.

The right-associative nature of ivy means that first argvInts and fileInts are combined and then the result of that combination is combined with oddInts leaving a single vector that consists of all of the elements of the three vectors (including any duplicate elements).

The application of unique to this combined vector gives us our value for combinedInts.

combinedInts = unique oddInts, argvInts, fileInts
'CombinedInts' outArray combinedInts
CombinedInts[11]: 3, 5, 7, 31, 33, 44, 55, 66, 8, 10, 12

Our three input vectors were each of length 4 but our resulting vector is of length 11. Both fileInts and oddInts contained an element 5 and this has been collapsed into a single element by the application of unique.

Calculate the Average

We need to calculate the average of our combined vector. To do this we need to sum the elements of combinedInts and divide the sum by the number of elements it contains.

Counting the number of elements in a vector is easy, we can do this with the unary operator count.

count combinedInts
11

Ivy has no traditional looping statements but it contains a number of operators that operate along the axes of a vector. In this simple example we have been working with vectors of a single dimension but Ivy can handle multiple dimensions.

The operator / (known as Reduce) would be called an adverb in J because it modifies the operation of a verb. In Ivy, Reduce modifies the behaviour of an operator. In the construction of our outArray operator we used Reduce to modify the behaviour of our comma operator. Here we are going to use Reduce to modify the operation of the ‘+’ operator. The expression

combinedInts 
3 5 7 31 33 44 55 66 8 10 12
+/ combinedInts 
274

is equivalent to laying out the elements of combinedInts with the ‘+’ operator between them and so it calculates the sum of the elements. We can use this to calculate the average of the elements. Our first attempt might be:

average = (+/ combinedInts) / count combinedInts
'Average: ',  text average
Average: 274/11

This reminds us that Ivy uses exact rational arithmetic and so in order to see our average as a floating point number (for comparison with our J calculation) we need to convert part of it to float. We can do this with the unary operator float:

average = (+/ combinedInts) / float count combinedInts
'Average: ',  text average
Average: 24.9090909091

Split into Higher and Lower Vectors

The last part of the program is to split the combinedInts vector into two distinct vectors with one vector containing all the integers that are higher than the average and the other containing the remaining integers.

The binary operator ‘>’, when applied to a vector A and an element B will result in a vector of the same length as A but with a one in each position where the element of A was greater than B and a 0 elsewhere. So evaluating combinedInts > average results in this:

combinedInts 
3 5 7 31 33 44 55 66 8 10 12
combinedInts > average
0 0 0 1 1 1 1 1 0 0 0

We can use this with the sel operator (see above) to extract just those elements that are higher than average using bracketing to force the desired interpretation:

higher = (combinedInts > average) sel combinedInts
'Higher: ', text higher
Higher: 31 33 44 55 66

To calculate the rest we make use of two new operators: in and not.

When applied to vectors A and B, A in B will result in a vector of the same length as A but with a 1 in each position where the element in the corresponding position in A was also in the vector B and a 0 in each position where the element in the corresponding position in A was not found in B.

combinedInts 
3 5 7 31 33 44 55 66 8 10 12
higher
31 33 44 55 66
combinedInts in higher
0 0 0 1 1 1 1 1 0 0 0

This tells us which elements are in higher, but we want to know which elements are not in higher. We can reverse the result using the not operator. The not operator will replace all of the 0s in a vector with 1s and all of the 1s in a vector with 0s:

not combinedInts in higher
1 1 1 0 0 0 0 0 1 1 1

Combining this with the sel operator:

lower = (not combinedInts in higher) sel combinedInts
'Lower: ', text lower
Lower: 3 5 7 8 10 12

In Summary

There were many things that made working with Ivy a really pleasurable experience, and only one or two things that didn’t.

  • Ivy’s use of relatively short operator names means that expressions, although not as terse as those of J, are still concise and this keeps the code window small and readable.
  • The source code for Ivy, written in Go, is very readable and so it is possible to get an understanding of what is happening behind the scenes. This was not the case with the J source code - at least, I didn’t think so.
  • Rob Pike describes Ivy as a plaything and a work in progress. A few people have cloned Ivy and added their own extensions but I really hope that Rob is going to extend the language a little further.
  • I found that there were some calculations that I did not feel I could perform in Ivy. This is quite likely as a result of my inexperience with APL-like languages. But, for example, the lack of an ‘each’ iterator stumped me on a few occasions.
  • Ivy is described, on its GitHub page as a calculator and it is a very capable calculator. It think it is fair to say that is a gross understatement - Ivy is much more than that.