Tacit programming

Tacit — or point-free — code names the operations, never the data. Instead of {(+/x)%#x}, with its explicit x, you write the mean as a pure combination of verbs: (+/;%;#). Amber is an array language, so most of it is already tacit; 2.0.1 adds the last piece — real trains (hooks and forks) — so a whole class of little functions needs no argument name at all.

New in 2.0.1. A parenthesised, semicolon-separated list whose every element is a function now runs as a train when applied: (f;g) is a hook, (f;g;h) a fork. Everything else on this page — adverbs, composition, projections — works in every version.

Why Amber is already tacit

In a scalar language you loop; in an array language you name a verb and it runs over the whole vector. That alone removes most of the bookkeeping that forces you to mention data. A verb applied to a vector, an adverb applied to a verb, one verb feeding another — each is a value you can name and combine without ever writing an index or a loop variable.

amber>
+/ 1 2 3 4          / 10        sum: the verb + folded by the adverb /
#? 3 1 3 2 1        / 3         count of distinct: two verbs, no argument named
|/ 5 2 9 1          / 9         maximum

Tacit style in Amber rests on three building blocks — adverbs, composition, and projection — and then trains, which combine them.

Adverbs — the workhorses

An adverb takes a verb and returns a new verb that applies it in a particular shape. They are the reason array code rarely needs an explicit loop.

AdverbNameExampleResult
/over (fold)+/ 1 2 3 410
\scan+\ 1 2 3 41 3 6 10
'each#'("ab";"cde")2 3
':each-prior-': 10 15 13 2010 5 -2 7
/:each-right10 +/: 1 2 311 12 13
\:each-left1 2 3 +\: 1011 12 13

-': (subtract-each-prior) is deltas; +\ is a running total; f/: and f\: build the outer-product tables. None of them mention an index.

Composition

Verbs read right to left, so writing them next to one another already composes them: #?x is #(?x) — "count of the distinct of x". Wrapping a run of verbs in space-separated parentheses makes that composition a value you can name and pass around.

amber>
(,|) 1 2 3          / ,3 2 1     enlist of the reverse: ,(|x)
last:(*|)           / first of the reverse = last element
last 1 2 3 4        / 4

Watch the semicolon. (f g) with a space is ordinary composition f(g x). (f;g) with a semicolon is a hook — a train, described next. They are different constructs; the semicolon is the switch.

Projection & currying

Supplying some of a verb's arguments returns a new verb waiting for the rest — a projection. This is how you turn a dyadic verb into a one-argument transform without naming anything.

amber>
(10*) 1 2 3         / 10 20 30   multiply-by-ten
(2+) 1 2 3          / 3 4 5      add-two
{x*x}' 1 2 3 4      / 1 4 9 16   a lambda under each
f[x;;z]             / a three-arg verb with the middle slot left open

Projections are ordinary values, so they slot straight into adverbs and — as you will see — into trains: (10*) is a perfectly good train element.

Trains — hooks and forks

A train is a parenthesised, semicolon-separated list whose every element is a function. When you apply it, Amber does not index the list — it wires the verbs together. Two shapes exist: the two-verb hook and the three-verb fork.

Fork (f;g;h)

A fork sends the argument through the two outer verbs and combines the results with the middle one:

(f;g;h) y  ≡  (f y) g (h y)

y f h f y h y g (f y) g (h y)

That single rule gives you a surprising number of everyday functions for free:

amber>
avg:(+/;%;#)        / mean = sum % count
avg 2 4 6 8 10      / 6.0

(|/;-;&/) 5 2 9 1    / 8          range = max - min

(&/;+;|/) 2 9 4      / 11         min + max  (halve it for the midpoint)
{x%2} (&/;+;|/) 2 9 4   / 5.5

Hook (f;g)

A hook applies g to the argument and then feeds both the original argument and that result to f:

(f;g) y  ≡  y f (g y)

amber>
(,;|) 1 2 3         / 1 2 3 3 2 1    a vector followed by its own reverse
(~;|) 1 2 1         / 1              palindrome test: y ~ (|y)
(~;|) 1 2 3         / 0

norm:(%;+/)         / divide each element by the total
norm 3 1 4 1 5      / 0.214 0.071 0.286 0.071 0.357
+/ norm 3 1 4 1 5   / 1.0            ... so it sums to 1

Dyadic trains

Trains are ambivalent. Called on two arguments — write them in bracket form t[x;y] — the argument on the left threads in as well:

shapemonadic t ydyadic t[x;y]
(f;g) hooky f (g y)x f (g y)
(f;g;h) fork(f y) g (h y)(x f y) g (x h y)
amber>
(+;*;-)[10;3]       / 91         (10+3) * (10-3)
(+;|)[100;1 2 3]    / 103 102 101    100 + |1 2 3

Monadic application also works by plain juxtaposition (t y); the bracket form is only required when you are passing two arguments. Infix (x t y) is reserved for verbs, so pass a train's two arguments in brackets.

Building point-free pipelines

Because a train is just a value, it composes with everything else. Elements can be primitives, derived verbs (+/), projections (10*) or full lambdas, and the whole train can be named, stored in a list, or wrapped in a lambda to run under an adverb.

amber>
(10*;+;{x*x}) 3               / 39        mixes a projection, a primitive and a lambda

/ to map a train over many rows, wrap it in a lambda -- see the note below
{(+/;%;#) x}' (1 2 3;4 5 6;10 20 30 40)   / 2.0 5.0 25.0   the mean of each row

Rules, edges & gotchas

  • Semicolons make a train; spaces make a composition. (f;g) is a hook; (f g) is f(g x). Reach for the semicolon when you want a fork or a hook.
  • Only two or three elements, and every one must be a function. A list of four or more verbs, or any list containing a non-function value, is left alone and indexes exactly as before — (10 20 30)[1] is still 20.
  • A train is applied, not adverb-modified. train'rows does not attach the each adverb to a list; wrap it — {train x}'rows — as in the pipeline example above.
  • Indexing a two- or three-element list of functions now trains it. This is the one behavioural change in 2.0.1. If you genuinely keep a dispatch table of two or three functions and index it by position, give it a fourth entry, or select with .[tbl;i], so it is not read as a train. In practice dispatch tables are dictionaries (`a`b!(f;g)), which are unaffected.

Quick reference

FormMeaningExample → result
f/ f\ f'over · scan · each+/ 1 2 36
f': f/: f\:each-prior · each-right · each-left-': 2 5 92 3 4
(f g)composition f(g x)(*|) 1 2 3 44
(v f), f[a;;c]projection / currying(10*) 330
(f;g)hook — y f (g y)(%;+/) 1 30.25 0.75
(f;g;h)fork — (f y) g (h y)(+/;%;#) 2 4 64.0
(f;g;h)[x;y]dyadic fork — (x f y) g (x h y)(+;*;-)[10;3]91

See also the Language & qSQL reference, and the CHANGELOG for the 2.0.1 implementation notes.