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
Goal: Go Array Language
Goal is an APL-like language, modelled on K and written in Go.
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,
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"
and1 2 3 4
, it would return:Initial [4]: 1, 2, 3, 4
Assign a small array of ints, say, the array:
31 5 7 6 3 2 8
to the variableints
.Use the
outArray
function to output theints
array with the title “Initial”.Create a new array,
sortedInts
, from the originalints
which 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 fromsortedInts
that 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
, andfileInts
arrays into a single array,combinedInts
, without duplicates and output it with the title “CombinedInts”.Calculate the average of the numbers in the
combinedInts
array and store it in the scalaraverage
.Split the numbers in the
combinedInts
array into two distinct arrays,higher
andlower
such thathigher
contains those ints that are higher than the value inaverage
andlower
contains the remaining ints. Output bothhigher
andlower
with `outArray’.
The Goal implementation is:
: {
outArray[title; ints]
"%s[%d]: %s"$(title; #ints; csv ,ints)
print }
: 31 5 7 6 3 2 8
ints["Initial"; ints]
outArray
: ints[<ints]
sortedInts["Sorted"; sortedInts]
outArray
: sortedInts[&2!sortedInts]
oddInts["Odd"; oddInts]
outArray
: "i"$'1_ARGS
argvInts["ArgvInts"; argvInts]
outArray
: *"i"$csv 'read "./fileints.csv"
fileInts["FileInts"; fileInts]
outArray
: ?oddInts, argvInts, fileInts
combinedInts["CombinedInts"; combinedInts]
outArray
: {(+/x)%#x} combinedInts
average"average: %f"$average
say
: {x@&x > average}combinedInts
higher["Higher";higher]
outArray
: {x@&~x in higher}[combinedInts]
lower["Lower";lower] outArray
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.
[7]: 31,5,7,6,3,2,8
Initial[7]: 2,3,5,6,7,8,31
Sorted[4]: 3,5,7,31
Odd[4]: 2,3,4,5
ArgvInts[4]: 5,55,25,15
FileInts[9]: 3,5,7,31,2,4,55,25,15
CombinedInts: 16.333333
average[3]: 31,55,25
Higher[6]: 3,5,7,2,4,15 Lower
The OutArray Function
: {
outArray[title; ints]
"%s[%d]: %s"$(title; #ints; csv ,ints)
print }
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:
: 31 5 7 6 3 2 8
ints["Initial"; ints] outArray
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
.
: ints[<ints]
sortedInts["Sorted"; sortedInts] outArray
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:
: 31 5 7 6 3 2 8
ints<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:
[<int] ints
is equivalent to:
[5 4 1 3 2 6 0] ints
and results in:
2 3 5 6 7 8 31
Select the Odd Integers
: sortedInts[&1=2!sortedInts]
oddInts["Odd"; oddInts] outArray
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.
: "i"$'1_ARGS
argvInts["ArgvInts"; argvInts] outArray
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
: "i"$'1_ARGS argvInts
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:
: "i"$1_ARGS argvInts
But is serves to illustrate the use of each
and so has been left in.
Reading from a File
: *"i"$csv 'read "./fileints.csv"
fileInts["FileInts"; fileInts] outArray
We read the contents of the file “./fileints.csv” with:
"./fileints.csv" read
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:
"./fileints.csv" csv read
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:
: *"i"$csv 'read "./fileints.csv" fileInts
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 (?).
: ?oddInts, argvInts, fileInts
combinedInts["CombinedInts"; combinedInts] outArray
First the arrays are joined into a single array and then the duplicates are removed by applying the distinct
verb.
Calculate the Average
: {(+/x)%#x} combinedInts
average"average: %f"$average say
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:
: (+/combinedInts)%#combinedInts
average: {(+/x)%#x} combinedInts
average: {(+/x)%#x}[combinedInts]
average: {(+/x)%#x}@combinedInts
average: {(+/x)%#x}.,combinedInts average
Split into Higher and Lower Arrays
: {x@&x > average}[combinedInts]
higher["Higher";higher]
outArray
: {x@&~x in higher}[combinedInts]
lower["Lower";lower] outArray
Given our variable average
that contains our calculated average and our combinedInts
array:
"average: %f"$average
say ["CombinedInts"; combinedInts] outArray
gives:
: 16.333333
average[9]: 3,5,7,31,2,4,55,25,15 CombinedInts
The expression
> average combinedInts
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 > average combinedInts
gives:
31 55 25
Tidying it up and placing into a function:
: {x@&x > average}combinedInts
higher["Higher";higher] outArray
gives:
[3]: 31,55,25 Higher
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 in higher combinedInts
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
:
: {x@&~x in higher}[combinedInts]
lower["Lower";lower] outArray
gives:
[6]: 3,5,7,2,4,15 Lower
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.