Tacit programming#

Tacit programming is programming without (direct) reference to the argument(s). Of course, you still need to get the data somehow, but the idea is that a function refers to the result of that function when applied to the argument(s) instead of just referring to itself. When you actually need to refer to an argument, you still need to apply a function to it, but since you want nothing done to the data, you’ll need an identity function. Dyalog APL gives you and which are left and right identity, respectively. This may seem trivial, but becomes very important later.

Next, we need to understand how a train (sequence) of functions is applied to the argument(s). Since APL functions can be called monadically or dyadically (niladic functions cannot directly be used in trains), there needs to be some rules. We also need a way to specify if we want any subsequent functions to be applied to the result of the previous functions, or on the argument(s) anew.


Let’s begin with 3-trains, or f g h. They tend to be the simplest to understand. In the following, we’ll call the left and right arguments A and B respectively. First up is the (albeit slightly more complicated) dyadic case, as the monadic case follows very easily from the dyadic one.

Evaluating A (f g h) B from the right, we first have h which represents A h B. Then we move on to g which will evaluate to f g (A h B). So we need to evaluate f first. f behaves just like h, in that it refers to A f B. Finally, g can be evaluated as (A f B) g (A h B).

Note that there is no confusion between this last non-tacit (or explicit) expression and a train. You can always tell the difference between explicit and tacit APL by looking at the rightmost token. If it is an array, it is explicit, otherwise it is tacit. Conversely, this also means that you need to separate a train from any data you want to apply it to, either by naming it in a separate statement, or by parenthesising it. Getting confused regarding this is a very common mistake.

Going back to our f g h train, what happens in the monadic case? The dyadic was (A f B) g (A h B), and the monadic is exactly the same, but with the As removed: (f B) g (h B). This applies universally to all trains: The parsing is identical for monadic and dyadic calls; the functions that would address the left argument are just called monadically. This also means that refers to the right argument when the train is called monadically.

3-trains are known as forks because their structure resembles a fork (like a rail switch) in that the middle function “connects” to the two sides. We can use the interpreter to help us display a visual representation of a fork:

]box on -t=tree    ⍝ Enable tree-display for tacit functions
+×÷                ⍝ A fork
Was ON -trains=tree
+ × ÷


This leads us to 2-trains. Consider f g h again — (A f B) g (A h B). What if there was no f? I.e. we just have g h. Since g would address its left argument, but there isn’t any, it is just called monadically, i.e. A (g h) B is g (A h B). This is known as an atop because the g is evaluated atop (i.e. on the top of) the result of h’s application.


Let’s look at 4-trains. (1-trains are simply single functions.) Consider f g h j. We begin from the right and grab up to three functions, i.e. g h j. Those are evaluated as before. Let’s call the result H. Now we have f H. Really, f would have taken a left argument, but there isn’t any, so it is just applied to H monadically. In total, f g h j is f (g h j) or to be explicit, A (f g h j) B is f ((A g B) h (A j B)).

One little exception which fits right in: The left side of the 3-train (left tine of a fork) may be a constant (i.e. not a function that is applied to the argument(s). It is then treated as if there had been a function there which gave that result. Here’s an illustration: A (42 g h) B is just like A ({42} g h) B where {42} is an ambivalent function which returns a constant value. So it all becomes 42 g (A h B) or (A{42}B) g (A h B) if you want.

Note that you cannot have a 2-train with a constant left side, like 42 f. Neither can you have a middle tine be a constant, like f 42 g. Nor can you have a right hand side be a constant, as that would make your code explicit, as per above. So what if you need a constant right-tine? For example, for a “divide-by-42” function? ⊢÷42 won’t work (it’ll give you the identity of the reciprocal of 42). Then you need to supply the constant as a left tine, and swap the arguments of the middle tine, using the (Commute) operator: 42÷⍨⊢.


Finally, let’s have a look at a 5-train, which completes the pattern. f g h j k: again we begin from the right and take three functions. Now we have f g (h j k). h j k evaluates as a normal 3-train, and its result (let’s call it J) becomes the right argument of g, so f g J. Then the pattern just repeats. A 4-train is an atop of a fork, and a 5-train is a fork of a fork, and a 6-train is an atop of a fork of a fork, etc.

Tacit rules#

Let’s look at some handy identities.

  • Because (f g) B is f (g B) then if g is , then (f g) is just f.

  • Because A (f g h) B is (A f B) g (A h B) then if f is and h is , then (f g h) is just g.

  • Because A (f g h) B is (A f B) g (A h B) then if f is and h is , then (f g h) is just g⍨.

We could, of course, make many more such identities, but I’m sure you get the idea, so just one more:

  • Because (f g) B is f g B and f∘g B is also f g B, we can substitute (f g) with f∘g in monadic cases.

Converting dfns to tacit#

OK, let’s look at the dfn given here:


Note that converting to tacit form doesn’t always make the code shorter. This is just for the exercise. We can begin by substituting for every (the right argument). That gives us (,⍨⍴⊢↑⍨×⍨)⌈.5*⍨≢⊢ which won’t work because of how trains are evaluated, so let’s fully parenthesise it:


Note that the left parenthesis is already a train, but this still doesn’t work, because that train used the constant , which we’ve substituted with a . But inside the train refers to the train’s own right argument, and we want the original right argument. So we need to “feed” the left train the unadulterated argument:


But now we get another issue: the functions in that train assumed the train was called monadically. That’s not the case any more, so let’s insert some tacks to use the correct arguments:

 ((,)⍴⊣↑(×)) ((.5*(≢⊢)))

OK, that was the left side. Now for the right side. (≢⊢) becomes just as per above identity, and the rightmost parenthesis isn’t needed:

 ((,)⍴⊣↑(×)) (.5*)

Now we can see that is applied monadically to its right argument, so we can glue to to the left train instead:

 ((,)⍴⊣↑(×)) (.5*)

Of course, we can remove that rightmost parenthesis too:

 ((,)⍴⊣↑(×)) .5*

That’s it. But we can do a little better. Note that ,⍨ and ×⍨ are “selfies”. It should be obvious that f⍨ X is the same as X f X (no matter if X is a function or a constant), so we can just substitute that:

 ((⊢,⊢)⍴⊣↑(⊢×⊢)) .5*

Now we can remove final unneeded parenthesis and the whitespace:


There you go. Totally unreadable, but it looks cool!

       ⊢ ∘    ┌───┼─┐
      ┌──┴──┐ 0.5 ⍨ ≢
  ┌───┼───┐ ⌈   ┌─┘
┌─┼─┐ ⍴ ┌─┼───┐ *
⊢ , ⊢   ⊣ ⍨ ┌─┼─┐
        ┌─┘ ⊢ × ⊢

Let’s do one more: Moris Zucca’s dfn {⊃⍵[(⍳⍴⍵)~⍵⍳⍵]}.

Right away we can spot an issue: you can’t use bracket indexing in a train, but luckily there is a functional alternative in the primitive. So, first let’s substitute that in:


Now, let’s do our ⍵→⊢ substitution:


Just a couple of things to fix in this one: ⍳⍴⊢ won’t work, and is called monadically, but we can easily fix those:


Now we’ve got an f case in (⍳⍴)⊢, so we’ll simplify as per the identity above:


Since (⍳⍴) is called monadically, we can use ⍳∘⍴:


Note that the rightmost uses the same left and right argument, so it is a selfie: ⍳⍨


Finally, is called monadically, so we can glue it to ⌷⍨:

⊃ ┌─┼──────┐
  ⊢ ∘   ┌──┼─┐
   ┌┴┐  ∘  ~ ⍨
   ⍨ ⊂ ┌┴┐ ┌─┘
 ┌─┘   ⍳ ⍴ ⍳

Here’s another. My dfn {⍵⊆⍨(⍴⍵)↑⍺/+\⍺}. This one is fun. Let’s start with substitution:


OK, on the right we have a monadic + so we’ll need to parenthesise it:


But now note that / is used as a function. However, it prefers to be an operator, i.e. doing reduction instead of replication. To force it into function mode, we need to make it the operand of an operator (since operators cannot be operands). We can use the trick that f⍨⍨ is the same as f (in dyadic cases):


But since we’re anyway swapping arguments (twice) we may as well just swap the actual and (+\⊣) instead:

⊢ ⍨  ┌──┼──────┐
┌─┘ ┌┴┐ ↑ ┌────┼─┐
⊆   ⍴ ⊢  ┌┴┐   ⍨ ⊣
         \ ⊣ ┌─┘
       ┌─┘   /