Home. Work-in-progress documentation for Goal. Last update: 2024-09-17.

5 Differences from K

This chapter gives a tour of the main differences with other K-like languages. It uses ngn/k as reference, because it’s the one I know the best.

5.1 New Features

5.1.1 Atomic strings

One of the main differences in Goal is that strings are atoms and are handled as such by the primitives. Most have specific behavior for strings, providing built-in support for common string-handling functionality. Those primitives generalize to arbitrary lists of strings when possible. A few of those are summarized in the following table:

s+s

concatenate strings

s*i

repeat string

i!s

pad string fields with spaces

!s

split a string into Unicode-space-separated fields

=s

split a string into lines (handles "\r\n" too)

s-s

trim suffix (if present)

s_s

trim prefix (if present)

s^s

trim a string on both sides using a cutset

s#s

number of non-overlapping instances of a substring

&s

number of bytes

s?s

index of a substring

_s

map Unicode letters to lower case

uc s

map Unicode letters to upper case

Generally, there’s some mnemonic for each of those meanings. For example = is drawn with two lines, so =s splits into lines (it could be seen as a kind of grouping by, so somewhat related to =d); #X is used for count, so s#s does substring count; !s produces a list from an atom, like !i; and so on.

The format/cast/parse verb $ has some new functionality, including sprintf-like formatting. Also, the sub verb provides various string substitution facilities, including regexp support.

It’s worth noting that, although strings are atoms, Goal provides "b"$ and "c"$ to transform from and to arrays of bytes or codepoints respectively, so array-like processing for strings is still possible when appropriate.

Also, note that because strings are already atomic, Goal does not support symbols. If interning is really necessary for performance reasons, classify % manually the strings into integers.

5.1.2 String quoting constructs

Goal comes with more flexible and expressive string quoting constructs, supporting the same set of escapes as Go’s double-quoted strings (including Unicode escape sequences, see this section), as well as Perl-like variable interpolation, like for example in "some $var". Additionally, there is a Perl-like qq/text $x/ form that allows for various kinds of delimiters, not only the slash. Also, there is a raw string quoting construct rq/raw string/ without escapes nor interpolation and with custom delimiter.

5.1.3 Regular expressions

Regular expressions are a built-in type in Goal. They can be built either at runtime with rx s, or using regexp literals of the form rx/PATTERN/, like rx/\s+/. The latter are compiled and checked at compile-time, avoiding both the need to pre-compile regexps for several uses, and issues with escaping special characters.

5.1.4 Error handling

Goal makes a distinction between fatal errors, often due to programming errors, and other kinds of errors, for example from I/O. The former are just panic strings, while the latter can represent arbitrary kinds of values and are callable in a special way to produce an error message suitable for user consumption. In addition, Goal provides some specific syntax support for making error handling less verbose using a 'expr statement similar to ? in Rust, but using prefix form. Note that there’s no confusion with the adverb each ', which has to follow a verb or noun (without spaces).

See the How do error values work? question of the FAQ for more details.

5.1.5 Dict syntax and field expressions

In addition to the dict ! verb, Goal supports a special syntax for defining (and amending) dicts ..[a:expr1;b:expr2], as well as (field) expressions ..expr that allow to evaluate things under a dict by using the key names as variables (as long as they are identifier-like strings). Expressions are actually syntax sugar for a regular lambda projection with some additional convenience features, including referring to arguments x, y and z like in lambdas, and special prefixes p. and q. to either project (like in compositions) or insert as-is (quote) regular variables, making expressions usable as a limited form of immutable closure.

See the question about field expressions in the FAQ for more details.

5.2 Miscellaneous Changes

5.2.1 No digraphs: Each, Windows and Shifts

In Goal, all adverb digraphs were removed. You still can append a colon and use +: to refer to the monadic version of a primitive. Because there are no digraph adverbs, they also follow the same convention as verbs: they are dyadic by default, but accept a colon at the end to become monadic: this is a minor difference from K.

Both each-left \: and each-right /: can be done using ` and ´. The second one is a common character found even in Latin1, but it is not ASCII! As an alternative, each-right can be obtained by projecting onto the left argument and regular each f', as in f[x;]'. It can often be simplified further as in (a+)' for primitive verbs.

The use of each-prior ': is replaced by the windows verb i^Y and the shift verbs « and » (with ASCII alternatives shift and rshift). Both approaches do have their strong and weak points. The rationale here for Goal was that the shifting verbs are more general and allow for more varied shifts of any size. Verbs are also somewhat easier to use than adverbs. Also, I don’t want digraphs, and I don’t know which symbol I could have used for the adverb.

Last but no least, binsearch is now done with the X$y verb form, and the first bin gets index 0, instead of -1, like in BQN.

5.2.2 Minor differences in tacit verb trains

Tacit compositions work mostly the same in Goal, except for adverbs being dyadic by default and needing a : for the monadic version, which is possible because adverb digraphs are gone. Also, in Goal compositions are just sugar for a lambda or a lambda projection. As a result, we get precise locations for any errors, and access to debugging tools such as \x or early-return.

5.2.3 Group by, index-count

The group verb = in Goal uses BQN’s semantics. It works on indices only, and returns an array of groups based on those indices, not a dictionary. As a bonus, negative indices can be used to discard values. Also, Goal only has the “group by” variant, using either the =d form, that groups dictionary keys using values for group indices, or the f=Y form, that groups Y using indices f@Y. The =I form is used for index-counting, often called unwhere, similar to freq #'= in K, but working on indices only, and producing an array. Grouping indices can still be done with {=(!#x)!x}.

This approach seems more Unix-like to me, in that it doesn’t need to do any kind of self-search on the values: that goes into separate verbs, like distinct or the new self-classify %X, equivalent to {x?x}. While a bit more low-level and verbose in some cases, Goal’s group makes it easy to implement various kinds of groupings, including “group by” functionality for tables. Also, K’s semantics can still easily be obtained by combining group with self-searching verbs: for example {(?x)!=(!#x)!%x} for K’s =X. See lib/k.goal at the root of the distribution for efficient user-defined equivalents of K’s “group”, “group keys by” and “freq”.

5.2.4 Tables and dicts

While Goal doesn’t have a dedicated table type, several primitives (like for filtering and indexing) offer table-like functionality when dict keys are strings. Along with field expressions, this allows for quite concise table manipulation in Goal.

5.2.5 Domain-swap and self-dict

Because Goal doesn’t have a dedicated table type, only dictionaries, the +d form swaps keys and values, instead of producing a table. Another nice new verb form for dicts is self-dict .X for lists, equivalent to {x!x}.

5.2.6 Zero values

Goal makes much less use of null values than ngn/k. For example, outdexing results in various kinds of zero values, depending on the array type. For example, numeric arrays give 0 and string arrays give "". See the How do zero value fills work? question of the FAQ for details.

One nice thing about this approach is that outdexing the result of group will always return an empty array. Another advantage is that zero is common to all numeric types, including the smallest ones.

Also, the new i@y form, called take/pad, takes advantage of zero-values. It’s like i#y, but when going out of range, instead of repeating elements, it pads with zero value fills. It’s similar to y@!i, but it avoids actually generating indices.

5.2.7 Numeric conversions

In Goal, floating point numbers that can be represented as an integer can be used anywhere the equivalent integer can be used. This makes it so that, except for operations that can overflow, the user doesn’t have to think about numeric representations.

One thing worth noting is that flat numeric arrays cannot be generic: if there’s a mix of integers and floats, all values will become floats. This restriction rarely matters, and simplifies both implementation and semantics. Also, for convenience, ~ returns true when matching integers with equivalent floating point numbers.

5.2.8 Take, Drop, Without

There are some differences in these primitives. Without X^Y has its arguments reversed with respect to ngn/k, for consistency with the renamed weed out f^Y and the symmetry with the new X#Y that does intersection now, like it did in K9 last time I read about it. Note that, for consistency, X#d removes from d entries with keys not in X, which is different from ngn/k, though it will probably only matter when there are duplicate keys. The f_Y form now does “drop where”, which is more in line with the I_Y form.

In Goal, there are also two new filtering forms X#t and X^t that provide a select-where kind of functionality for tables, which together with field expressions allows for convenient table processing.

Also, reshape was removed, replaced with “cut shape” i$Y, which can cut into lines (positive i), or columns (negative i).

5.2.9 Sort and Grade for dicts

Sorting works similarly as in ngn/k, except that ^X was added as a means to sort a list without grading it, and <d and >d return dictionary results, preserving both keys and values. Searching for null values is much less useful in Goal, because few primitives can generate them (namely, parsing with "n"$). It’s still possible to find 0n values with the new nan keyword. The null integer value, called 0i in Goal (following the type name), can be searched for with equality too.

5.2.10 Cond ? and logical and/or

Cond is written with ?[cond;then;else] instead of $[cond;then;else]. For convenience, there are also short-circuiting and[x;y;…] and or[x;y;…], which are nice for handling error cases.

5.2.11 New false values

Goal introduces three new false values in conditionals: 0i, 0n, and -0w. The reason behind this change for 0i is that it makes |/!0 return a false value, avoiding a typical problematic edge case. The other two new values were added for consistency, so that numeric null and smallest values are all false, though they’re more unlikely to be used in conditionals.

5.2.12 List syntax uses left-to-right evaluation

In Goal, expressions in a list are evaluated left-to-right and are like sequences that collect all the values. This is more convenient when using the list syntax together with assignments, in particular in multi-line lists.

The only case of right-to-left evaluation in Goal happens for arguments due to such order being the most useful in dyadic infix operations, given verb associativity rules.

5.2.13 Amend assignment is just sugar around amend

I’m not sure if all K implementations do the same thing about this, but in Goal, the x[y]op:z form is equivalent to x:@[x;y;op;z]. It returns the whole modified array, not just the parts that where modified.

5.2.14 There is no splice triadic form

Splice is mainly useful for array-style text-handling. String-handling primitives cover that usage in Goal.

5.2.15 Rank-insensitive find ?

Some K dialects, like ngn/k, have a rank-sensitivity concept, that makes X? behave like an inverse of X@ and facilitates searching for non-atomic values, at the cost of not working for generic lists with mixed “ranks”.

The find dyad ? is rank-insensitive in Goal. That means that the result always has the same length as the right argument and is always flat. Find works for any kind of value, but searching for a single non-atomic value requires enlisting it.

5.2.16 No deep-where

Because Goal uses monadic & pervasively for strings, having deep-where would not be very natural given its dependency on the rank concept (which we don’t follow for find ? either).

5.2.17 Get Global, Eval and Parse

The .s form in goal has the same meaning as for symbols in K: it gets the value of a named global.

The usual general eval is done using the eval s form in Goal.

There is also a new eval[s;loc;pfx] triadic form. The location string loc serves two purposes: it is used to provide error locations and to avoid evaluating twice a same location in the main context. The prefix string pfx is used for globals in s during evaluation. This new form of eval is the basis for the new import keyword, that provides a more typical import mechanism.

Goal also provides "v"$s as a partial inverse for $x that works on values whose atoms are only of numeric, string or regexp type. It does not support arbitrary execution of Goal code, but it can be implemented more efficiently than general evaluation, without requiring byte-compilation nor execution. It can be used as a serialization method with acceptable performance and the advantage of better matching Goal’s types than for example json.

5.2.18 Input/Output with keywords

Interacting with the OS is done using keywords in Goal. There’s currently support for typical input and output operations, running commands and pipes, and interacting with the environment. There’s currently no networking support. Until then, because Goal is usable as a library, such things could be added by a user using Go’s standard library.