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:
- Strings in goal are Unicode aware and are one of the atomic types of the language.
- Many verbs have specific behaviour for strings.
- In addition to regular expression string matching the language supports a flexible string formatting and interpolation syntax.
- An atomic error type can be matched and handled with a conditional.
- Goal can be embedded in a Go program and extended by Go code.
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.
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:
Write a function,
outArraythat, 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"and1 2 3 4, it would return:Initial [4]: 1, 2, 3, 4Assign a small array of ints, say, the array:
31 5 7 6 3 2 8to the variableints.Use the
outArrayfunction to output theintsarray with the title “Initial”.Create a new array,
sortedInts, from the originalintswhich contains the elements but sorted into ascending order, and output it with the outArray function using the title “Sorted”.Create an array,
oddInts, that contains just those ints fromsortedIntsthat are odd and output it with the title “Odd”.Create an array,
argvInts, that has been read from the command line and output it with the title “argvInts”.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”.Combine the
oddInts,argvInts, andfileIntsarrays into a single array,combinedInts, without duplicates and output it with the title “CombinedInts”.Calculate the average of the numbers in the
combinedIntsarray and store it in the scalaraverage.Split the numbers in the
combinedIntsarray into two distinct arrays,higherandlowersuch thathighercontains those ints that are higher than the value inaverageandlowercontains the remaining ints. Output bothhigherandlowerwith `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,15The 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
<intsgives
5 4 1 3 2 6 0If 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!sortedIntsgives:
0 1 1 0 1 0 1In 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!sortedIntsgives:
1 2 4 6Indexing 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 5then 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_ARGSdrops 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_ARGSwill set argvInts to the integer array with value:
2 3 4 5This 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_ARGSBut 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"$averageWe 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}.,combinedIntsSplit 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,15The expression
combinedInts > averageresults 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 0We can find the indices of these ones using (‘&’) where:
&combinedInts > averageThese are the indices into combinedInts at which elements that are greater than average can be found:
3 6 7Indexing combinedInts using these indexes:
combinedInts@&combinedInts > averagegives:
31 55 25Tidying it up and placing into a function:
higher: {x@&x > average}combinedInts
outArray["Higher";higher]gives:
Higher[3]: 31,55,25To find those elements that are not higher than average we calculate:
combinedInts in higherwhich returns:
0 0 0 1 0 0 1 1 0which 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 highergiving:
1 1 1 0 1 1 0 0 1We can express these 1s as indexes using where:
&~combinedInts in higherwhich gives:
0 1 2 4 5 8Using these to index into combinedInts:
combinedInts@&~combinedInts in highergives us those integers in combinedInts that are not in higher:
3 5 7 2 4 15Wrapping 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,15In 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.