Tacit programming

Tacit code, also called point-free, names the operations and 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

One catch: kept in a name, (*|) grabs the two-argument | (max), so last 1 2 3 4 would hand you back a projection. The : in (*|:) pins | to its one-argument meaning, reverse. Applied right away, (*|) 1 2 3 4 is fine as it is.

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. Applying it does not index the list. Amber 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, written in bracket form as 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, so (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 as {train x}'rows, like 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 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 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.