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.
?
and logical and
/or
?
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. Because strings are atoms, those primitives are pervasive (string-atomic) when possible. The main ones are summarized in the following table:
|
concatenate strings |
|
repeat string |
|
match glob pattern |
|
pad string fields with spaces |
|
split a string into Unicode-space-separated fields |
|
split a string into lines (handles
|
|
trim spaces right |
|
trim suffix (if present) |
|
trim prefix (if present) |
|
trim a string on both sides using a cutset |
|
number of non-overlapping instances of a substring |
|
number of bytes |
|
index of a substring |
|
map Unicode letters to lower case |
|
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
line number, 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 idempotent
"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,
""\
can be used to split a string into a list of 1-char strings.
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.
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.
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.
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.
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.
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
lib/mods.goal
file provides common adverb combinations involving
each-right as user-defined functions.
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 not least, binsearch is now done with the
X$y
verb form, and the first bin gets index
0
,
instead of
-1
,
like in BQN.
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.
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”.
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.
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}
.
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.
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.
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
).
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.
?
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.
Goal introduces new false values in conditionals like
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 numeric false 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.
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.
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.
Splice is mainly useful for array-style text-handling. String-handling primitives cover that usage in Goal.
?
#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.
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).
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
.
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.