Skip to content

What Use Are User-Defined Operators?

Vector 16:1, page 61

programming techniques language design

Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 61

Dyalog APL permits user-defined operators. Apart from the header syntax, and the fact you need to type: )ops or: ⎕NL 4 instead of: )fns or: ⎕NL 3 to see their names, they are written and maintained just like functions. A lot of APLers I know claim to have no real use for them. But I make a great deal of use of user-defined operators and I wouldn’t be without them.

I write a lot of code which is called by other APL programmers through a public interface, i.e. a pre-arranged collection of function (or operator) calls. Often I don’t get to talk to the programmers calling my code. Eventually it gets maintained by someone else. If he/she ever gets to talk to me, then that’s a failure on my part. APLers who have never written for such an environment don’t appreciate what’s involved. Unlike C++, the essential apparatus for large-scale code support doesn’t come free with APL. But it’s not difficult to organise.

Trawling through my old code, I notice my operators fall into the following (non-exclusive) categories:

  1. variants of primitive operators
  2. 3 or 4 argument functions
  3. common code
  4. function jigs

Now I don’t have much use for 1, but I include it as a simple way of introducing the idea, plus exercising some terminology. First some implicit definitions...

An APL function foo takes a right argument ⍵ and maybe also a left argument ⍺. You declare ⍺ to be optional by enclosing it in braces in the function header. Otherwise, when called, both ⍺ and ⍵ must be supplied. So these are the options:

[0]   z←foo ⍵
[0]   z←⍺ foo ⍵
[0]   z←{⍺} foo ⍵

An APL operator oppo takes a left operand, f and maybe also a right operand, g. Both f and g are thought of as being APL functions, but when oppo is called you can supply variable names instead, or even constants, whereupon f behaves like a (function) argument, assuming the correct name class for its contents. There is no construct corresponding to {⍺} to show one of the operators is optional. So when called, both f and g must be supplied if both are specified. So these are the options:

[0]   z←(f oppo) ⍵
[0]   z←(f oppo g) ⍵
[0]   z←⍺ (f oppo) ⍵
[0]   z←⍺ (f oppo g) ⍵
[0]   z←{⍺} (f oppo) ⍵
[0]   z←{⍺} (f oppo g) ⍵

The result of executing an operator with given operands is actually an APL function, called the derived function. You can use it in a functional assignment like this to create a new function foo:

      foo←(f oppo)

but the typical use is with both operands and arguments to return a value, just like a function call.

The result is that oppo behaves like a 3 (or 4) argument function. Except that one (or two) of the ‘arguments’ are really operands, and you can supply the name of a function or a functional expression which will appear inside oppo like a localised function...

      z←x(f oppo)y
      z←x(1∘+∘f oppo)y

Variants of Primitive Operators

Let’s take as an example the (primitive) operator: (¨) {each}. Its behaviour is not to everyone’s liking, because using it to apply a function foo to each element of an arbitrary list y is not what you’d expect if you’re thinking of it as a :For-loop. If the list y becomes empty you might hope that foo would not get called, whereas it does — once, with an argument of scalar 0. Now, caring nothing for APL theology (which tells you infallibly that this is what you ought to expect and you should jolly well write foo to accommodate it) you decide to write a replacement: PEACH (polite-EACH) which does what you want, i.e. it doesn’t call foo if the right argument y is empty.

      ∇PEACH[⎕]∇
[0]   z←(foo PEACH)y
[1]   z←⍬
[2]   :For j :In y
[3]   z,←⊂foo j
[4]   :EndFor

As a special case of foo we’ll define a fn: inc, which simply bumps its (scalarised) arg by 1...

      ∇inc[⎕]∇
[0]   z←inc y
[1]   z←1+⊃y

Now, by putting a stop on line 1...

      1 ⎕stop'inc'

You can verify that the next expression calls inc 3 times, the second one calls inc once, but the third one doesn’t call inc at all...

      (inc PEACH) 2 4 5
      inc¨⍬
      (inc PEACH)⍬

To demonstrate the existence of the intermediate derived function, assign it to a name like foo:

      foo←(inc PEACH)
      foo 2 4 5
3 5 6
      (inc PEACH)2 4 5
3 5 6
      (1∘+∘inc PEACH)2 4 5
4 6 7

You can put a stop on inc and see again the above calling behaviour. Note that alterations to inc are correctly reflected in foo, so it’s not like ordinary functional assignment (which takes a snapshot of any function employed, so it does not keep in step with updates).

3 or 4 Argument Functions

One limitation of infix notation is that it allows functions to take no more than two arguments. Now of course you can call a function with any number of logically distinct arguments, concatenating them into a single nested right argument. The function can then take this apart by a threaded assignment.

Thus:

      x←foo u v w

[0]   z←foo y;u;v;w
[1]   u v w←y

However, when you’re writing public functions to be called by others, who may not be people of your intelligence :-) it’s nice to leave it to the interpreter to tell them they’ve not supplied an argument...

[0]   z←u(v oppo)w
...
      x1←u(v oppo)w
      x2←(v oppo)w
SYNTAX ERROR

Common Code

Throwaway code has no use for this. You can get away with replicating a function foo to make a slightly different one goo. But when you’re writing long lasting code, which may be maintained by chimpanzees at some time in the future, sometime someone’s going to edit foo and not realise that goo needs a similar change.

Here’s a facile example of common code. Two functions, next and prev, are only slightly different. Here they just alter a global XX, but a more complex algorithm can be readily imagined with today’s window applications. However instead of replicating next to create prev, both are defined on a common operator: step. Now of course this could be done without writing a user-defined operator (step could simply be a dyadic function) but in some cases (where next and prev are themselves dyadic) defining common code as an operator is a preferable solution.

In my experience, it’s worth writing even the most complicated common-code operator to accommodate all the differences between next and prev, because it does at least document these differences all in one place. In other words, never replicate common code, if you want it to stay common.

      ∇next[⎕]∇
[0]   next n
[1]   (+step)n

      ∇prev[⎕]∇
[0]   prev n
[1]   (-step)n

      ∇step[⎕]∇
[0]   (foo step)n
[1]   'XX was:'XX
[2]   XX←XX foo n
[3]   'XX now:'XX

      XX←1
      next 2
 XX was:  1
 XX now:  3
      prev 1
 XX was:  3
 XX now:  2

Function Jig

This is my favourite. The difference between this and the foregoing examples is in the programmer’s intention. Here the operator is a bolt-on ‘jig’ for executing the operand. The algorithm does not logically need it. The function which serves as its operand is the thing that does the real work. The operator is there to modify its behaviour, like changing its environment, e.g. ⎕IO or ⎕ML. Its syntax intentionally makes it look like a bolt-on. It accepts the arguments intended for the original function and simply passes them through (Dyalog APL manages to achieve this without replicating the values in the workspace). In this example it allows recovery (sometimes) in the event of a WS FULL. In practice I find that some memory-intensive expressions may give WS FULL, but after garbage collection triggered by ⎕WA it executes successfully. Such as this innocuous looking expression:

      B←×C

(where C is usually a very large Boolean array, but we want to be quite sure B is Boolean).

      ∇wa[⎕]∇
[0]   z←(foo wa)z;rainyday;x;⎕TRAP
[1]   ⍝Apply z-filter: foo, with WS FULL handling
[2]   ⍝NB: foo must be monadic in this variant of: wa
[3]    ⎕TRAP←1 'C' '⎕EX''rainyday''⋄⎕TRAP←0⍴⎕TRAP⋄→AGAIN'
[4]   ⍝trap (1=) WS FULL
[5]    rainyday←1000⍴2 ⍝--safety buffer to release on WS FULL
[6]    z←foo z
[7]    →0
[8]   AGAIN: ⍝--WS FULL: try again...
[9]    hint'...garbage collecting, please wait...'
[10]   x←⎕WA ⍝--collect garbage
[11]   ⎕TRAP←1 'C' '⎕TRAP←0⍴⎕TRAP⋄→EX' ⍝--trap WS FULL, timeout etc
[12]   z←foo z ⍝--try again
[13]   →0
[14]  EX: ⍝--admit failure...
[15]   ∘ ⍝--crash, in this instance.

Note the use of rainyday, a so-called ‘rainy-day fund’ of emergency memory. Some WS FULL conditions may lock up nearly all ws memory, preventing a recovery function from doing even the simplest housekeeping. A rainy-day fund can let it soldier on. To use this operator, replace the WS FULL-prone function call, (say):

      z←foo x

with:

      z←(foo wa)x