Transforms
Among arr.ai's powerful features, transform operators are perhaps the most heavily used. They come in four flavors, each operating on a different kind of input:
->
transforms a single value.=>
transforms the members of a set.>>
transforms the entries of a sequence or dictionary.:>
transforms the attributes of a tuple.
The ->
transform operator has the following syntactic structure:
lhs -> expr
binds the name.
tolhs
and evaluatesexpr
in this context.lhs -> \pattern expr
bindspattern
tolhs
and evaluatesexpr
in this context.
The other three operators follow the same syntax. However, they bind each of the
elements of the lhs
, not the entire lhs
.
If this isn't clear, don't worry, it'll become clear as we work through the examples below.
#
The basicsLet's explore the basic usage of the above operators, one at a time.
->
#
The arrow operator, ->
, can seem almost trivial:
@> 42 -> .@> 42 -> . + 1@> "hello, " -> . ++ "world"@> (x: 2, y: 3, z: 5) -> .x * .y + .z
The last example above illustrates the utility of the .
name binding, since
.x
is shorthand for (.).x
.
The alternative form is likewise fairly trivial:
@> 42 -> \x x + 1@> (x: 2, y: 3, z: 5) -> \t t.x * t.y + t.z
In this case, instead of the name .
being bound, the name x
is bound to 42
when evaluating the expression x + 1
. This can come in handy when there are
transform operators nested inside each other and an expression needs to refer to
multiple levels:
@> (x: 3, y: 5, z: 5) -> ((x: 2, y: 2) -> \c ((c.x - .x)^2 + (c.y - .y)^2)^0.5) * .z
The ->
operator shows its usefulness when lhs
becomes more complex:
@> /set data = (customer: (custid: 123, name: (first: "Franz", last: "Kafka")))@> data.customer.name -> $`${//str.upper(.last)}, ${.first}`
As you can see, ->
offers a very lightweight mechanism to refer to a common
complex expression while allowing a reader to follow a natural left-to-right
order as they seek to understand the intent.
On the flip side, when a complex expression spans multiple lines of code, the
let
operator is usually preferred:
@> let v = (x: 3, y: 5, z: 5); > ((x: 2, y: 2) -> \c ((c.x - v.x)^2 + (c.y - v.y)^2)^0.5) * v.z
While let . = ...
is also allowed, it is not considered idiomatic arr.ai.
=>
The "set-arrow" operator, =>
, requires lhs
to be a set. It applies the
transform to each member of lhs
and evaluates to a set of the results:
@> {2, 4, 6, 8, 10} => . + 1@> {2, 4, 6, 8, 10} => . % 6
Note that, if the transform produces the same output for some of the lhs
elements, the resulting set will have fewer members than lhs
. Remember that
sets cannot express the notion of duplicate membership, so if two or more input
values yield the same output value, the resulting set will contain that value
without reference to how many occurrences contributed to it.
>>
#
The "sequence-arrow" operator, >>
requires lhs
to be a binary relation with
@
as one of its attributes. This includes arrays, dictionaries, strings and
byte arrays. It transforms the value associated with each @
.
As a refresher, remember that arrays are just sets whose tuples have two
attributes: @
denoting a position within the array and @item
denoting the
value at that position. Similarly, strings, are just sets whose tuples have
attributes @
and @char
. And likewise for byte arrays (@
and @byte
) and
dictionaries (@
and @value
).
@> {(@:0, @item:1), (@:1, @item:2), (@:2, @item:3), (@:3, @item:4)}@> {(@:0, @char:115), (@:1, @char:116), (@:2, @char:114), (@:3, @char:105), (@:4, @char:110), (@:5, @char:103)}@> {(@:0, @byte:98,), (@:1, @byte:121), (@:2, @byte:116), (@:3, @byte:101), (@:4, @byte:115)}@> {(@:[-37.8, 145], @value:'MEL'), (@:[-33.9, 151.2], @value:'SYD'), (@:[51.5, -0.1], @value:'LCY')}
The >>
operator can be thought of as transforming just the value instead of
the key:
@> {(@:0, @item:1), (@:1, @item:2), (@:2, @item:3), (@:3, @item:4)} >> 2 ^ .@> [1, 2, 3, 4] >> 2 ^ .@> ["s", "toe", "thumb"] >> . ++ "nail"@> {"A": 10, "B": 42, "C": 100} >> 100 - .@> {[-37.8, 145]: 'mel', [-33.9, 151.2]: 'syd', [51.5, -0.1]: 'lcy'} >> //str.upper(.)
>>>
#
The >>>
operator is a variant of the >>
operator. It performs the same basic
operation, but also makes the @
attribute available to the transformation
expression. This form is special in that it must be a two-argument function.
@> ['red', 'green', 'blue'] >>> \i \c $'${c} = ${i}'@> /set cities = {[-37.8, 145]: 'MEL', [-33.9, 151.2]: 'SYD', [51.5, -0.1]: 'LCY'}@> cities >>> \[lat, lng] \code $'${code} is at (${lat}, ${lng})'
Note that >>>
still transforms only the associated value. @
remains
unchanged. If you want to transform the entire tuple, use =>
instead.
:>
#
The "tuple-arrow" operator, :>
, requires lhs
to be a tuple. It applies the
transform to each attribute value of lhs
and evaluates to a tuple of the results:
@> (r: 0.5, g: 0.2, b: 0.7) :> 1 - .(b: 0.3, g: 0.8, r: 0.5)
#
Unary formsThe above operator can be used in unary form. For instance, => \x 2 + x
. In
all cases, the lhs
is implied to be .
. This property allows the transform
operators to be chained together to transform deeper structures:
@> [{1, 2}, {2, 3, 4}, {1, 5}] >> => 10 + .@> {(r:0.7, g:0, b:0), (r:0.4, g:0.6, b:0), (r:0.5, g:0.5, b:1)} => :> . ^ 2
#
Interaction with order and orderby(The following discussion applies equally to order
and orderby
, so we'll
only discuss orderby
.)
The orderby
operator transforms a set into an array:
@> {'red', 'green', 'blue'} orderby .@> {(r:0.7, g:0, b:0), (r:0.4, g:0.6, b:0), (r:0.5, g:0.5, b:1)} orderby .r@> {(r:0.7, g:0, b:0), (r:0.4, g:0.6, b:0), (r:0.5, g:0.5, b:1)} orderby .g
Things can get a bit confusing when applying orderby
to a sequence or
dictionary. It is important to remember that orderby
always operates on the
underlying set, never on the abstraction it represents. For instance, consider
the following expression:
[9, 4, 2, 4] orderby .
At first glance, it seems that this will reorder the numbers of the array. However, when you try it out, you'll notice that the output is in fact an array of the tuples underlying the original array, not an array of the numbers it represents.
In order to rearrange the values accordingly, you must explicitly deal with the
underlying representation of arrays. While this can be confusing at first, it is
a result of consistently applying the rule that orderby
operates on sets. It
also gives the programmer full control over how ordering should behave. In the
above case, a question arises: do you want two instances of the number 4 in the
result? There isn't a single correct answer. If you want an array of all scores,
you can do it like this:
@> [9, 4, 2, 4] orderby [.@item, .] >> .@item
Let's unpack this one step at a time. orderby [.@item, .]
evaluates to an
array ordered first by .@item
, which is the number, and then by .
, which is
the whole tuple. The purpose of the .
is to act as a tie-breaker. Without it,
the results will behave strangely in future. You could also use .@
as the
tie-breaker in this case, since .@
will be unique for all input elements.
However, this assumes regular arrays in which all values of .@
are unique, and
this is not guaranteed in arr.ai. It's generally safest to use .
as a
tie-breaker, since it is guaranteed to be unique, regardless of the input set's
structure.
As observed earlier, the result is an array of the tuples underlying the
original array, so we use >> .@item
to extract just the numbers from that
intermediate result.
On the other hand, if you only want an ordered array of the scores that were achieved and don't want to see duplicate scores repeated, it's simply a matter of getting a set of the scores and ordering them:
@> [9, 4, 2, 4] => .@item orderby .