Goal: Go Array Language

Goal is an APL-like language, modelled on K and written in Go.

APL
Goal
Author

John Bates

Published

December 20, 2023

In previous notebooks I wrote about my experiences with J and Ivy, both APL-like languages. Goal, like Ivy, is also written in Go. Unlike Ivy, Goal is based on the the K programming language which is itself a variant of APL.

Goal builds on the K language by both extending and simplifying. Extensions include:

The differences between Goal and K are documented here.

The main influences of Goal are the ngn/k dialect of K and BQN.

Once you start to look at these APL-like languages you discover what a web of variants exist. The best place to discover them is to browse the APL Wiki, or the K Wiki.

A simplified view of the APL variant genealogy can be derived from the data here.

The diagram below uses a subset of this data to show the relationships between a number of popular APL variants. An arrow from A to B indicates that variant B inherits some of its properties from variant A.

flowchart TB
APL(APL: A programming language) --> APL360(APL360)
APLUS(A+) --> BQN(BQN)
APLUS --> K(K)
APL360 --> APLPLUS(APL PLUS)
APL360 --> J(J)
APL360 --> MATLAB(MATLAB)
APL360 --> Mathematica(Mathematica)
APL360 --> R(R)
APLPLUS --> NARS(NARS)
APLPLUS --> SHARP(SHARP APL)
BQN --> Goal(Goal)
J --> BQN
J --> Dyalog(Dyalog APL)
Dyalog --> BQN
K --> Q(Q)
K --> Klong(Klong)
K --> NGN(ngn/k)
K --> oK(oK)
NGN --> Goal
Mathematica --> Julia(Julia)
MATLAB --> Julia
NARS --> Ivy(Ivy)
R --> Julia
SHARP --> J
SHARP --> Dyalog
SHARP --> APLUS

style Goal fill:lightgreen
style Ivy fill:lightgreen
style J fill:lightgreen
style K fill:lightgreen
Note

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

I was interested to see how Goal would cope with my simple array manipulation task that I had described in my lure of J and Ivy notebooks. This notebook is a record of how that task can be performed using Goal.

Simple Array Manipulation Task

That task involved the following steps - their implementation in Goal is given below:

  1. Write a function, outArray that, given a string title and an array of integers as its two parameters, outputs that array 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 array of ints, say, the array: 31 5 7 6 3 2 8 to the variable ints.

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

  4. Create a new array, 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 an array, oddInts, that contains just those ints from sortedInts that are odd and output it with the title “Odd”.

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

  7. Create an array, 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 array, 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 Goal implementation is:

outArray: {
    [title; ints]
    print "%s[%d]: %s"$(title; #ints; csv ,ints)
}

ints: 31 5 7 6 3 2 8
outArray["Initial"; ints]

sortedInts: ints[<ints]
outArray["Sorted"; sortedInts]

oddInts: sortedInts[&2!sortedInts]
outArray["Odd"; oddInts]

argvInts: "i"$'1_ARGS
outArray["ArgvInts"; argvInts]

fileInts: *"i"$csv 'read "./fileints.csv"
outArray["FileInts"; fileInts]

combinedInts: ?oddInts, argvInts, fileInts
outArray["CombinedInts"; combinedInts]

average: {(+/x)%#x} combinedInts
say "average: %f"$average

higher: {x@&x > average}combinedInts
outArray["Higher";higher]

lower: {x@&~x in higher}[combinedInts]
outArray["Lower";lower]

If we invoke goal with the arguments ‘2 3 4 5’ and if the current directory contains a CSV format file named “fileints.csv” containing the line: 5,55,25,15 we will see the following output.

Initial[7]: 31,5,7,6,3,2,8
Sorted[7]: 2,3,5,6,7,8,31
Odd[4]: 3,5,7,31
ArgvInts[4]: 2,3,4,5
FileInts[4]: 5,55,25,15
CombinedInts[9]: 3,5,7,31,2,4,55,25,15
average: 16.333333
Higher[3]: 31,55,25
Lower[6]: 3,5,7,2,4,15

The OutArray Function

outArray: {
    [title; ints]
    print "%s[%d]: %s"$(title; #ints; csv ,ints)
}

We define outArray to be a function (that is what the curly braces indicate) that takes two parameters title and ints. The last line of the function is its return value. The $ verb, with a format string on its left and one or more values on its right is known as format. It returns a formatted string value in much the same way that sprintf might in a C-like language. This format string expects 3 arguments, a string, an integer and another string: namely, the title, a count of the number of integers in ints and a list of the integers in comma-separated format.

The monadic verb # applied to an array is known as length and gives the length of the array. So #ints is the number of integers in the array.

The monadic verb csv when applied to a generic array returns a string containing the array values in comma-separated format. The monadic verb ,, known as enlist, converts our integer array into a generic array. The monadic verb print prints its argument to standard output. (The monadic verb say, seen later, does the same as print but appends a newline.)

Assignment and Output

We can define an array, ints, and display it with our function:

ints: 31 5 7 6 3 2 8
outArray["Initial"; ints]    

A variable name followed by a colon (:) is known as assign. The variable named on the left is assigned the value of the expression on the right. OutArray is just a variable that has been assigned the value of a lambda (function).

Sort the Array

We can sort the integer array and assign the sorted array to the variable sortedInts.

sortedInts: ints[<ints]
outArray["Sorted"; sortedInts]

The monadic verb <, known as ascend, when applied to an array of integers, ints say, gives an array of the same length in which the value of each element of the new array is an index into the original array.
Picking elements from ints using that array of indexes will result in an array of the elements in the original array arranged in ascending order. (Indexes in Goal start at zero)

So for example:

ints: 31 5 7 6 3 2 8
<ints

gives

5 4 1 3 2 6 0

If we pick the 5th, 4th, 1st, 3rd, 2nd, 6th and 0th elements from ints we get:

2 3 5 6 7 8 31 

So <ints is an array of 7 indexes into ints. We can pick the values at each of these indexes by ‘indexing’ our ints array with the list of indexes:

ints[<int]

is equivalent to:

ints[5 4 1 3 2 6 0]

and results in:

2 3 5 6 7 8 31 

Select the Odd Integers

oddInts: sortedInts[&1=2!sortedInts]
outArray["Odd"; oddInts]

Remainder modulus 2 is obtained with the dyadic ! verb such that 2!sortedInts results in an array of 0s and 1s of the same length as sortedInts but with a 0 in the place of every integer that is even and a 1 in the place of every odd integer. So, given sortedInts (2 3 5 6 7 8 31)

2!sortedInts

gives:

0 1 1 0 1 0 1

In which each position that contains a 1 corresponds to an odd number in sortedInts. The monadic verb &, known as where, when given an array of 0s and 1s returns an array of those indexes that contain a 1. So:

&2!sortedInts

gives:

1 2 4 6

Indexing sortedInts by this array gives us an array of those elements that are odd.

Reading from the Environment

Access to the arguments with which the Goal script was invoked is obtained through the global variable ARGS. If Goal is invoked as

goal intlist.goal 2 3 4 5

then ARGS contains an array of the following string values: "intlist.goal" "2" "3" "4" "5". We would like to discard the first element which contains the script name and convert the remaining elements to an integer array. The dyadic verb _ when presented with an array as its right argument, y say, and an integer as its left argument, x say, is known as drop and will return a new array made from all but the first x elements of y.

argvInts: "i"$'1_ARGS
outArray["ArgvInts"; argvInts]

So

1_ARGS

drops the first element of ARGS and returns the result as a new array. But this is an array of strings and we want an array of integers.

The dyadic adverb ', with a verb on its left and an array on its right is known as each. Each is similar to the map function in many other functional languages - it will apply the verb to each of the elements of the array resulting in a new array.

The monadic verb “i”$ when given a string as its argument is known as parse and, more specifically, the dyadic verb $ with its hardwired left argument of “i” will parse a string into an integer. So the combination of "i"$ and each will convert each string element into an integer element. Given the above value for ARGS

argvInts: "i"$'1_ARGS

will set argvInts to the integer array with value:

2 3 4 5

This assignment could be slightly simplified because "i"$ will operate on an array argument and so, rather than employing the use of each, we could have written the line as:

argvInts: "i"$1_ARGS

But is serves to illustrate the use of each and so has been left in.

Reading from a File

fileInts: *"i"$csv 'read "./fileints.csv"
outArray["FileInts"; fileInts]

We read the contents of the file “./fileints.csv” with:

read "./fileints.csv"

which returns the string:

"5,55,25,15"

We can convert that to an array of strings by using the monadic form of the verb csv which will parse a string into comma-separated components:

csv read "./fileints.csv"

gives:

,"5" "55" "25" "15"

This is potentially a multi-row array containing a row for each line of CSV content in the file. In our case we know that we only have a single line but we still have a 1x4 array of strings. We can convert this to a 1x4 array of integers with:

"i"$csv read "./fileints.csv"

And, finally, we can select the first row using the monadic first verb. So our final assignment looks like this:

fileInts: *"i"$csv 'read "./fileints.csv"

The apostrophe (’) character that was prepended onto the read verb is known as try. Try will cause an early return, with a suitable error message, if read returns an error. For example:

 "open ./fileints.csv: no such file or directory"

Without try control would continue into csv and beyond and the actual error message would be lost.

Combine without Duplicates

To combine three integer arrays into a single array and remove duplicates we make use of the dyadic join verb (,) and the monadic distinct verb (?).

combinedInts: ?oddInts, argvInts, fileInts
outArray["CombinedInts"; combinedInts]

First the arrays are joined into a single array and then the duplicates are removed by applying the distinct verb.

Calculate the Average

average: {(+/x)%#x} combinedInts
say "average: %f"$average

We construct a function to calculate the average value of an array of integers. Unless we declare the names of a function’s arguments they are assumed to be named x, y and z. If you want a function with more than 3 arguments you need to define the names of the arguments as we did for the outArray function above. Our average function looks like this (with spaces added for readability):

{(+/x) % #x}

The expression (+/x) calculates the sum of the integers in x. The expression #x calculates the length of x - the number of elements in x. The verb % performs floating point division. We have to bracket the components that calculate the sum because Goal is right-associative and without the bracketing the function would be parsed as (+/)(x % #x) which is not what we want.

The adverb ‘/’ is known as fold. With a verb on its left it applies the verb between each of the elements of the array on its right. So with the dyadic verb (+), known as add, the expression +/x sums the integers in the array x.

Our function is invoked with combinedInts as its x argument by what is known as “Implicit At Indexing” - When Goal sees two nouns together (in this case a function and an integer array) it assumes that we want to call the function with the array as its argument. Equivalent ways of invoking the function would be by using square brackets (M-expression), using ‘@’ (known as Apply At) or using ‘.’ (known as Apply). So all of the following are equivalent:

average: (+/combinedInts)%#combinedInts
average: {(+/x)%#x} combinedInts
average: {(+/x)%#x}[combinedInts]
average: {(+/x)%#x}@combinedInts
average: {(+/x)%#x}.,combinedInts

Split into Higher and Lower Arrays

higher: {x@&x > average}[combinedInts]
outArray["Higher";higher]

lower: {x@&~x in higher}[combinedInts]
outArray["Lower";lower]

Given our variable average that contains our calculated average and our combinedInts array:

say "average: %f"$average
outArray["CombinedInts"; combinedInts]

gives:

average: 16.333333
CombinedInts[9]: 3,5,7,31,2,4,55,25,15

The expression

combinedInts > average

results in an integer array in which each element of combinedInts which is greater than the average scalar value is replaced with a 1 and all other elements are replaced with a zero.

0 0 0 1 0 0 1 1 0

We can find the indices of these ones using (‘&’) where:

&combinedInts > average

These are the indices into combinedInts at which elements that are greater than average can be found:

3 6 7

Indexing combinedInts using these indexes:

combinedInts@&combinedInts > average

gives:

31 55 25

Tidying it up and placing into a function:

higher: {x@&x > average}combinedInts
outArray["Higher";higher]

gives:

Higher[3]: 31,55,25

To find those elements that are not higher than average we calculate:

combinedInts in higher

which returns:

0 0 0 1 0 0 1 1 0

which is an integer array containing a 1 for each element in combinedInts that is also in higher. But we want those elements that are not in higher.

We can apply not to this array:

~combinedInts in higher

giving:

1 1 1 0 1 1 0 0 1

We can express these 1s as indexes using where:

&~combinedInts in higher

which gives:

0 1 2 4 5 8

Using these to index into combinedInts:

combinedInts@&~combinedInts in higher

gives us those integers in combinedInts that are not in higher:

3 5 7 2 4 15

Wrapping it all up into a function and applying the function to combinedInts:

lower: {x@&~x in higher}[combinedInts]
outArray["Lower";lower]

gives:

Lower[6]: 3,5,7,2,4,15

In Summary

Much to my surprise, I found working with Goal to be more satisfying that working with both Ivy and J.

The reach of Ivy is (quite deliberately) somewhat limited and as a result I found that, when using Ivy, I was unable to perform some of the parts of this task.

At the other end of the spectrum, J is a more complex language to learn, having a more difficult syntax involving digraphs and the concept of rank which needs to be understood in order to handle higher dimensional arrays.

Goal feels as though it sits comfortably in between the two, is slightly more easy on the eye than J, and has much more functionality than Ivy. Goal would be my go-to APL-like language of choice.