Tradfns
Contents
Tradfns#
Tradfns are the original way to write your own functions in APL. Tradfns are procedural in style, unlike dfns, which are functional.
The basic structure of a tradfn is:
∇ header line
function body
∇
Function body#
Control structures#
Let’s consider the body first. We have available to us the full set of control structures from procedural languages. All such key words begin with a colon, :
, for example If … :EndIf
. Lines with such keywords must begin with the keyword, and have nothing else on them, although parameters (like a condition) are considered parenthesised expressions. For example,
∇ Ex ;i;j;k
:For i j k :In 'abc'(1 2 3)'ABC'
⎕←i j k
:EndFor
∇
Ex
abc 1 2 3 ABC
This assigns (i j k)←'abc'
during the first loop, then (i j k)←1 2 3
, etc. :For
can also “transpose”, using :InEach
instead of :In
which makes (i j k)←'a'1'A'
etc:
∇ Ex ;i;j;k
:For i j k :InEach 'abc'(1 2 3)'ABC'
⎕←i j k
:EndFor
∇
Ex
a 1 A b 2 B c 3 C
Any unpacking is possible, for example:
∇ Ex ;i;j;k
:For i(j k) :InEach (⍳3)('aA' 'bB' 'cC')
⎕←i j k
:EndFor
∇
Ex
1 aA 2 bB 3 cC
:If
, of course, has :Else
, but also :ElseIf
. While ∧
and ∨
are normal arithmetic functions, it is allowed to write one or more :AndIfs
or :OrIfs
which will shortcut. A quite common pattern used to check if a variable exists and then, for example, set it to a default value if it doesn’t:
∇ Ex ;state
:If 0=⎕NC'state'
state←42
:EndIf
⎕←state
∇
Ex
42
Ambivalence#
While dfns are always ambivalent (though ⍺
will give value error if called monadically and there’s no ⍺←
statement), Dyalog tradfns have to be explicitly declared ambivalent in the header: ∇result←{lAarg} FnName rArg
.
Then one can test for ⎕NC'lArg'
, but there’s also a faster way: 900⌶ which ignores its argument and returns whether the function was called monadically:
∇ res←{lArg} Ambiv rArg
:If 900⌶⍬
lArg←42
:EndIf
res←lArg ⍝ Return the left argument
∇
Ambiv 'hello'
99 Ambiv 'world'
42
99
Note that 900⌶
only works for tradfns, although dfns don’t need it so much since they have ⍺←
.
Advanced control structures#
:If
and :While
should feel familiar, but the :Select
statement warrants specification:
:Select expression
:Case value
:CaseList values
:Else
:EndSelect
No need “break”, like in C’s switch
statement. It jumps to the end when reaching the next case.
The conditional loops are a bit interesting in that you can piece them together as you want. You can begin with either :While condition
(which checks before it starts) or :Repeat
which doesn’t check. You can end with either :EndWhile/:EndRepeat
(which don’t check anything) or :Until
condition (which does). In other words, you can match :While
with :Until
. :While
and :Until
can also be followed by one or more :AndIfs
or :OrIfs
.
You can even insert statements between :If/:ElseIf/:While/:Until
and :AndIf/:OrIf
, but this can be hard to read. For example, consider the following:
∇ r←Foo val;b
b←1
:If 10<val
b←2
:AndIf 100>val
r←b,val
:Else
r←val,b
:EndIf
∇
Foo 5
Foo 50
Foo 500
5 1
2 50
500 2
The :AndIf
and :OrIf
allows you to build up Boolean expressions that have the same kind of short-circuiting behaviour as that found in mainstream languages, but with the added option of statements between them. Whilst this can be confusing to read, it has its place, for example, where you have some costly set-up code required in order to evaluate one of the expressions making up a boolean condition in an if-statement. You can do work that needs to be prepared so we’re ready to do the next check. For example,
:If ⎕NEXISTS file
content←⊃⎕NGET file 1
:AndIf ×≢content
Process¨content
:EndIf
That sort of thing would be painful to write in as a dfn.
You can do the same with loops, too:
∇ r←Foo val
r←val
:Repeat
r+←?5
:Until r>11
:OrIf r=9
∇
Foo 1
Foo ¯100
12
13
When looping, you can also continue with the next iteration without finishing this one, by stating :Continue
and you can quit the loop immediately with :Leave
:
∇ r←Foo
r←0
:While 1
r+←1
:If r>10
:Leave ⍝ Like 'break' in C or Python
:EndIf
:EndWhile
∇
Foo
11
Non-flow structures#
There’s actually another couple of interesting structures, which aren’t really flow control per se. :Section…:EndSection
is like :If 1
which is useful for organising your code, and they don’t need a comment symbol on their right. You can put any text there. The :Section
itself provides no actual visible functionality.
∇ r←Foo arg
r←arg
:Section We can group code that belongs together in sections
:If r>10
⎕←'Greater than 10'
:EndIf
:EndSection
∇
Foo 4
Foo 15
4
Greater than 10 15
:Trap
takes one or more error numbers exactly like dfns’ error guards. Then the main code, and then :Case
or :CaseList
with error numbers. You can also/instead use :Else
for all (other) errors.
Tradfns can also do advanced stuff that dfns can’t do. If you write :Implements trigger var
then the function gets called every time var is changed in that namespace.
∇ r←Foo
:Implements trigger var
⎕←'var changed!'
∇
var←0
var changed!
If you want a callback on all variable changes, you can use *
instead of a name. You can also use var1,var2
to only react to those. :Implements
is just a declaration, not a structure.
The header#
There can be up to four parts of the header:
result
calling syntax
locals
comment
Result#
The result is optional and must be terminated by ←
if present. It contains the result name or a parenthesised list of space-separated names.
If one needs to return a vector of various values, then using a name list is nice, because one can assign to each name separately, and only upon return are they collected together:
∇(vertices results)←…
vertices←…
results←
Fun fact: a name can occur multiple places in the header, including in a single name list, so you can actually write somewhat useful function without any body, just a header. For example, ∇(x x)←dup x
makes two of its argument. And (x y)←x juxtapose y
is the same as {⍺ ⍵}
.
The result can also be made “shy”, like a dfn that ends with an assignment {shh←42}
. This is done by putting the name or the name list in braces. For example, ∇{shh}←Shy shh
will silence its argument, but the value can still be coerced out.
If the result variable name is a function, then the function will return that function! Behold:
∇ Fn←PlusMinus
:If 1=?2
Fn←+
:Else
Fn←-
:EndIf
∇
Then 3 PlusMinus 4
will give either ¯1
or 7
, each time it is run, it is random.
3 PlusMinus 4
3 PlusMinus 4
3 PlusMinus 4
¯1
7
¯1
Calling syntax#
The calling syntax of the header is always be present. It is basically an image of how the function needs to be called. For example, a monadic function would have FunctionName argumentName
. A dyadic function would have leftArg FnName rightArg
. The right argument can also be a name list like the result. In that case, APL will refuse to call the function with anything but a vector argument of the correct length. This is pretty neat for “type” checking. A tradfn can be made ambivalent by putting braces around the left argument name, as we discussed before. The left and right arguments are not allowed to be the same, but multiple names in the right argument can be the same (last will prevail) which is convenient if you’re writing a function that needs to take multiple arguments, some of which it doesn’t need, for example, ∇ foo(important _ critical _ _)
.
A tradfn can be also be niladic, unlike a dfn. Then the syntax part is just the function name. This is usually used for returning caches, bootstrapping, constants, etc. Another useful thing is for a niladic tradfn is to return a derived function, since that allows you to use the editor on it, and also to construct it over multiple lines.
∇ f←Avg
sum←+⌿
count←1⌈≢
f←sum÷count
∇
So, about operators. The “central” part of the syntax declaration for an operator needs to be parenthesised. It then has two names for a monadic operator (Operand OPERATOR)
or three names for a dyadic operator (Operand1 OPERATOR Operand2)
. Outside the parenthesis there must be a name or namelist on the right for the right argument(s), and optionally an optionally optional left argument on the left. In other words, that is either no left argument or yes a left argument or a braced left argument.
Now we can also understand why allowing a left argument namelist would make it really hard to understand what the header stood for: things like (a b)(c d)
and (a b c)d e
would certainly be tougher to parse for humans. In practice, if multiple “arguments” are needed, people tend to use multiple right arguments. Of course, you can always unpack any array into any structure, not just a simple list.
As opposed to dfns, tradfns do not auto-localise. This means that it is important that you do so by declaring all your locals. After the syntax part, one can write one or more names, each prefixed by ;
to localise them. There’s no need to localise other names that occur in the header. They’re all local. The only exception is the function/operator’s own name. If you really want to reuse that name, you can localise it explicitly. As a relatively new feature (17.0), you can continue localising names up until you have any actual code (so comments and empty lines are fine):
∇foo;local
;more;locals
⍝ finally:
;last;ones
Finally, the header line allows a comment. Nothing fancy there. Just a comment :-)
So in summary:
∇{(result1 result2)}←{left}(Op1 OP Op2)(right args);local;local2 ⍝ comment