User defined functions
This is a fairly brief introduction, and there are exercises at the end of this section to help solidify your understanding, but there is also a more extensive treatment of user-defined functions in the book Mastering Dyalog APL.
The Three Function Styles
So far, we have been reading and writing dfns.
3 {⍺+⍵} 5 ⍝ Left argument ⍺, right argument ⍵
{⍵>0:⍵,∇ ⍵-1 ⋄ ⍵}5 ⍝ Guard is : (colon). The function itself is ∇ (del)
Fn ← {⍺⍵} ⍝ We can give functions names
It is also possible to name functions which do not explicitly refer to their arguments. This is called tacit or point-free programming.
Plus ← +
IndicesTo ← ⍳
_Reduce ← /
Sum ← Plus _Reduce
Sum IndicesTo 10
There is a syntax for composing functions called trains.
A two-train is an atop:
3(|-)5
2
|3-5
2
A three-train is a fork:
3(-×+)5
¯16
(3-5)×(3+5)
¯16
Any further functions simply alternate between atop (even number of functions) and fork (odd number of functions).
3(|-×+)5 ⍝ Absolute value of the product of sum and difference
16
3(⌈|-×+)5 ⍝ Max residue with the product of sum and difference
4
They allow some rather neat and memorable ways to write short functions.
Mean ← +⌿ ÷ ≢ ⍝ The sum divided by the count
Mean 3 1 4 1
3 (+,-) 5 ⍝ Plus and minus
','(≠⊆⊢)'some,text' ⍝ Split on commas
Note
Small unnamed dfns and tacit functions expand your vocabulary. One of my favourites is the "split by delimiter" train (≠⊆⊢)
. It looks like a beat-up face kaomoji. A similar phrase which can take multiple delimiters can be found on aplcart.info.
Traditional functions
Dyalog is a modern APL implementation. Since early APL implementations there has been a way of defining functions with a header line and named arguments and results. Since the introduction of dfns, functions of the original style are called traditional functions or tradfns.
Mean ← +⌿÷≢ ⍝ A 3-train (fork) for the arithmetic mean
Mean ← {(+⌿⍵)÷≢⍵} ⍝ A dfn for the arithmetic mean
∇ m ← Mean a ⍝ A tradfn for the arithmetic mean
m ← (+⌿a) ÷ ≢a
∇
Note
Copy and paste everything between (and including) the two ∇
del symbols into the session, and press Enter
, to define a tradfn in your workspace.
Using Shift+Enter
with the cursor on a name will bring up an editor window for that named thing.
A tradfn header reflects the calling syntax of the function.
∇ {result}←{optional}Tradfn argument;local1
[1] ;local2 ⍝ Locals can be declared across multiple lines in the header
[2] :If 0=⎕NC'optional'
[3] optional←'no left argument'
[4] :EndIf
[5] local1←'⍺: ',optional
[6] local2←'⍵: ',argument
[7] global←⍪'TradFn last called with'local1 local2
[8] result←⍪local1 local2
∇
Note
The ∇
del representation of the TradFn
function above is the vector representation result of ⎕VR'TradFn'
which can be directly input into the session.
- Try calling
TradFn
with one and two arguments. How can the result be made to display to the session? - Inspect the variable
global
after callingTradFn
with different arguments. - Step through the function using
Ctrl+Enter
. Inspect⎕NC'optional'
whenTradFn
is called with one argument and when it is called with two arguments.
Here is the smallest tradfn:
∇ T
∇
T
is a function which takes no arguments, returns no results and has no effects.
Results in {}
curly braces are called shy results and do not print to the session by default, but can be passed to arguments. To ease debugging and write functions with predictable behaviour, it is generally best not to use shy results.
Optional left arguments are a little awkward in tradfns. The dfn equivalent is a little nicer looking: {⍺←'default' ⋄ ⍺,⍵}
.
Name scope, locals and globals
The scope of a name describes the circumstances under which it is visible to code.
For most intents and purposes, you just need to know about the difference between how local and global names are defined in the syntax, and how name shadowing works.
By default, names assigned in tradfns are global. This is mostly for historical reasons. Names declared in the header - the arguments, results, and names preceded by semicolons - are localised.
Traditional function header example
result ← left FunctionName right;var1;var2
The header for a function called FunctionName
. The names result
, left
, right
, var1
and var2
are local.
By default, names in a dfn are local to that dfn. This is the preferred default in most modern programming languages.
If we define a name in the current namespace, that name is visible only within that namespace unless referred to by its full namespace path (e.g. #.nsref.var
).
'ns1'⎕ns⍬ ⋄ ns1.var←1 2 3
'ns2'⎕ns⍬ ⋄ ⎕cs ns2
⎕←var
VALUE ERROR: Undefined name: var
⎕←var
∧
⎕←#.ns1.var
1 2 3
Let us now define a dfn and a tradfn in #.ns1
:
⎕cs #.ns1
∇ Dfn←{
[1] var←'lexical'⍵
[2] }
∇
∇ Tradfn arg
[1] var←'dynamic'arg
∇
Note
While the ∇
del representation of dfns can be used to define dfns in the session, dfns in scripted namespaces must be defined without ∇
dels.
If we call each of these functions, Tradfn
will modify var
in the current namespace, but Dfn
will not:
Dfn var
var
1 2 3
Tradfn var
var
┌───────┬─────┐
│dynamic│1 2 3│
└───────┴─────┘
In the following definition, var⊢←
will update var
in the closest scope where it is localised - in this case #.ns1
.
∇ Dfn←{
[1] var⊢←'lexical'⍵
[2] }
∇
In Tradfns, references to local names within a function are said to "shadow" the same names from outer scopes. Notice how the following definition of Tradfn
fails.
∇ Tradfn arg;var
[1] var,←'dynamic'arg
∇
A similar dfn succeeds because, in dfns, modified assignment will search the local scope and then any parent scopes.
∇ Dfn←{
[1] var,←'lexical'⍵
[2] }
∇
For completeness, here we will also mention ⎕SHADOW
. It is used when names are dynamically created using ⍎
, ⎕FX
or ⎕FIX
but need to be localised. However, it is best to use the function syntax to establish name scope in general. Further information can be found in the specialists section on shadowed names in Mastering Dyalog APL.
The technical distinction between dfns and tradfns is that tradfns have dynamic scope whereas dfns have lexical scope.
For further explanation of how this affects the use of dfns, see section 5.5.3 of Mastering Dyalog APL.
Avoid globals
When possible, avoid using global variables. Pass parameters to functions as arguments unless this becomes very awkward. The use of global variables should be limited to state settings that affect the entire application, or tables containing databases that are shared globally. If you need global constants, it is a good idea to create them in a function in order to be able to use source code management / change tracking software.
A function which uses globals is difficult, if not impossible, to run in parallel. If two copies of the function run in parallel and they update the global data, some kind of locking is required. Locking often defeats the potential benefits of parallel execution.
Names should be localized unless they really really, really, really need to be global.
An example of when to use globals is a switch which affects the entire application:
∇ err←Log msg
[1] :If verbose
[2] ⎕←msg ⍝ Display information to the user
[3] :EndIf
[4] PrintFile msg
∇
Nested functions
It is possible to define functions inside some other functions.
-
Tacit functions can only include other user-defined functions by name
Sort ← {(⊂⍋⍵)⌷⍵} CSI ← Sort⍥⎕C ⍝ Case-insensitive sort
-
Dfns can contain tacit definitions and dfn definitions, as well as any named user-defined functions
SortedMeans ← { Sort ← {(⊂⍋⍵)⌷⍵} Mean ← +⌿÷1⌈≢ Sort Mean¨⍵ }
- Tradfns can contain tacit definitions, dfn definitions and any named user-defined functions
∇ result←SortedMeans vectors;Mean;Sort [1] Sort←{(⊂⍋⍵)⌷⍵} [2] Mean←+⌿÷1⌈≢ [3] result←Sort Mean¨vectors ∇
Which style to use?
While usage of different function styles varies throughout many applications, you might take inspiration from Adám's APL Style Guide, when writing brand new production code. When maintaining others' code, it is best to try to continue in the already established style.
Dfns
For medium sized functions and utilities. Nested dfns are fine, but never use multi-line dfns inline.
MultiDfn←{ ⍝ A Dfn with nested in-line multi-line dfns
(3{ ⍝ These are confusing to read and trace through
⍺+2×⍵
}⍺){
x←⍺-4+⍵
x-2×⍺
}3+⍵
}
Instead, give them names and then apply them. Named dfns should be multi-line so that they can be traced through, unless truly trivial.
MultiDfn2←{ ⍝ The previous function rewritten more clearly
y←3+2×⍺
x←y-1+⍵
x-2×y
}
Do not use a dfn instead of naming a variable. For example, instead of
r←{⍵/⍨10≤⍵}a,b
write
candidates←a,b
r←candidates/⍨10≤candidates
Tacit functions
Best as short, pure functions, performing some specific task such as data transformation. Trains and functions derived from functions and operators (e.g. +/
) can be used inline if they are not too complex.
Tradfns
Best used as program control and for dealing with system interactions. The use of control structures can make procedural tasks easier to debug. For example, if an error occurs during a loop or iteration.
¯5{i←⍺+⍳⍵ ⋄ i÷i-2}10 ⍝ A single line function cannot be traced through
Note
Use Ctrl+Enter
to step through a multiline function. You can then use Shift+Enter
to edit the function during execution and Esc
to save your changes to the function and continue execution.
∇ r←a MultiLineError o;i
[1] :For i :In a+⍳o
[2] r←i+3
[3] r÷r-2
[4] :EndFor
∇
Problem set 10
Which style again?
- Which of the following function styles can have multiple lines?
- TradFns
- Dfns
- Tacit functions
- Which of the following function styles can be anonymous (unnamed)?
- Tradfns
- Dfns
- Tacit
- Think about which function style would be most appropriate in the following situations.
- Launching an application
- Applying a mathematical formula to an array of values
- A utility function to sort an array
- Reading and writing files
- Expressing the sum of two functions (f+g)(x)
- Downloading data from the internet
- GUI programming
- Checking if a function is a no-op for a particular array
- Defining a piecewise mathematical function
Choo choo
- Translating functions
- Convert the following dfns into trains
{⌈/≢¨⍵}
{1+⍺-⍵}
{∨/⍺∊⍵}
{(⌽⍵)≡⍵}
- Convert the following trains into dfns
(⌈/-⌊/)
(+⌿÷1⌈≢)
(⊢-|)
(1∧⊢,÷)
- Convert the following dfns into trains
Marking Tests
Way back in problem set 3 you wrote a dfn to convert test scores into letter values.
You were led to produce some function or expression similar to the following:
Grade←{'FDCBA'[+/⍵∘.>80 70 60 50 0]}
Grade 95 65 92 77
This is an array-oriented solution to this problem. However, if a human was manually grading test scores, they might take one scored paper at a time and decide on which letter grade to write by reading each score.
Procedural pseudocode:
scores = 93,85,45,10,70,16,93,63,41,7,95,45,76
For each score in scores:
If score is greater than 80:
Write "A"
Else If score is greater than 70:
Write "B"
Else If score is greater than 60:
Write "C"
Else If score is greater than 50:
Write "D"
Else
Write "F"
Control Structures in Dyalog are keywords beginning with a :
colon.
:If :OrIf :AndIf :ElseIf :Else :EndIf
:For :In :EndFor
:While :EndWhile
:Repeat :Until :Return
-
Translate the pseudocode above into a tradfn called
Grade2
using control structures. -
Rewrite the
Grade
function again as either a dfn or a tradfn calledGrade3
which uses⍺⍸⍵
interval index. -
Use the
]runtime
user command to compare the computation time for each of the three grading functions.]runtime -c "Grade 10×⍳10" "Grade2 10×⍳10" "Grade3 10×⍳10"