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.
+/ 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 maximumTacit 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.
| Adverb | Name | Example | Result |
|---|---|---|---|
/ | over (fold) | +/ 1 2 3 4 | 10 |
\ | scan | +\ 1 2 3 4 | 1 3 6 10 |
' | each | #'("ab";"cde") | 2 3 |
': | each-prior | -': 10 15 13 20 | 10 5 -2 7 |
/: | each-right | 10 +/: 1 2 3 | 11 12 13 |
\: | each-left | 1 2 3 +\: 10 | 11 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.
(,|) 1 2 3 / ,3 2 1 enlist of the reverse: ,(|x)
last:(*|) / first of the reverse = last element
last 1 2 3 4 / 4Watch 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.
(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 openProjections 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)
That single rule gives you a surprising number of everyday functions for free:
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.5Hook (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)
(,;|) 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 1Dyadic 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:
| shape | monadic t y | dyadic t[x;y] |
|---|---|---|
(f;g) hook | y f (g y) | x f (g y) |
(f;g;h) fork | (f y) g (h y) | (x f y) g (x h y) |
(+;*;-)[10;3] / 91 (10+3) * (10-3)
(+;|)[100;1 2 3] / 103 102 101 100 + |1 2 3Monadic 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.
(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 rowRules, edges & gotchas
- Semicolons make a train; spaces make a composition.
(f;g)is a hook;(f g)isf(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 still20. - A train is applied, not adverb-modified.
train'rowsdoes 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
| Form | Meaning | Example → result |
|---|---|---|
f/ f\ f' | over · scan · each | +/ 1 2 3 → 6 |
f': f/: f\: | each-prior · each-right · each-left | -': 2 5 9 → 2 3 4 |
(f g) | composition f(g x) | (*|) 1 2 3 4 → 4 |
(v f), f[a;;c] | projection / currying | (10*) 3 → 30 |
(f;g) | hook — y f (g y) | (%;+/) 1 3 → 0.25 0.75 |
(f;g;h) | fork — (f y) g (h y) | (+/;%;#) 2 4 6 → 4.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.