Strategies for Dealing with Multiple Arguments: the Case for Names
Vector 18:3, page 64
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 64
Once upon a time, an APL function could take at most two arguments. If the logic of the function required a larger number of arguments, the programmer had to come up with a scheme by which all of them were represented by at most two homogeneous arrays, putting character data in the left argument and numeric data in the right argument or vice versa.
With the introduction of nested and heterogeneous arrays and strand notation, it became trivial to write functions that, from the user’s perspective, allowed arguments of any type or shape to be "strung out" as a strand. Each element could be assigned to its own variable internally, eliminating the necessity to "break" the logical arguments apart and rearrange them into their desired form. Today we can write functions that look like the following, which has, as far as a user (or calling function) is concerned, twelve arguments:
∇ Example1 args;title;per;ampl;vmin;vmax;voff;hmin;hmax;hoff;axes;ticks;color
[1] ⍝ Arguments:
[2] ⍝ [1] Title (use '' for blank)
[3] ⍝ [2] Period
[4] ⍝ [3] Amplitude
[5] ⍝ [4] Vertical minimum
[6] ⍝ [5] Vertical maximum
[7] ⍝ [6] Vertical offset
[8] ⍝ [7] Horizontal minimum
[9] ⍝ [8] Horizontal maximum
[10] ⍝ [9] Horizontal offset
[11] ⍝ [10] Displayed axes ('x'; 'y'; 'xy' or 'yx' for both; '' for neither)
[12] ⍝ [11] Tick interval (x,y; 0 for do not display)
[13] ⍝ [12] Colors (bg rgb triple, fg rgb triple)
[14] ⍝
[15] (title per ampl vmin vmax voff hmin hmax hoff axes ticks color)←args
[16] ⍝ Processing...
∇
To invoke this function, one supplies an argument strand containing twelve values:
Chances are, though, that the user of this function doesn’t really care about some, perhaps even most, of these argument values. We’d like to able to offer our users appropriate default values for any or all of them, allowing them to supply only those arguments that they are concerned about. To do this, we need only select some value that would be inappropriate for any of the arguments, and use it as a "placeholder" to represent a suitable default value. From the user’s perspective, this value represents "I don’t care". The refined function looks like:
∇ Example2 args;title;per;ampl;vmin;vmax;voff;hmin;hmax;hoff;axes;ticks;color;s
[1] ⍝ Arguments:
[2] ⍝ [1] Title (default '')
[3] ⍝ [2] Period (default 2×pi)
[4] ⍝ [3] Amplitude (default 1)
[5] ⍝ [4] Vertical minimum (default ¯1)
[6] ⍝ [5] Vertical maximum (default 1)
[7] ⍝ [6] Vertical offset (default 0)
[8] ⍝ [7] Horizontal minimum (default 0)
[9] ⍝ [8] Horizontal maximum (default 2×pi)
[10] ⍝ [9] Horizontal offset (default 0)
[11] ⍝ [10] Displayed axes: 'x'; 'y'; 'xy' or 'yx' for both; '' for neither
[12] ⍝ (default 'xy')
[13] ⍝ [11] Tick interval: x,y; 0 for do not display (default (0.2,0.2×pi)
[14] ⍝ [12] Colors: bg rgb triple, fg rgb triple (default (0 0 0)(255 255 255))
[15] ⍝ Use ⍬ for default value.
[16] ⍝
[17] (s/args)←(s←args≡¨⊂⍬)/'' (○2) 1 ¯1 1 0 0 (○2) 0 'xy' (.2,○.2)
((0 0 0)(255 255 255))
[18] (title per ampl vmin vmax voff hmin hmax hoff axes ticks color)←args
[19] ⍝ Processing...
∇
With this function, the user who cares about only a few values can specify only those, and allow the rest to take on their programmer-defined default values:
The user need only know where to put the values he (or she) does care about, and need not even know what the rest of the arguments represent.
He does, however, still need to know how many arguments are defined, and must type the appropriate number of trailing placeholders.
We can relieve him of these burdens easily enough:
∇ Example3 args;title;per;ampl;vmin;vmax;voff;hmin;hmax;hoff;axes;ticks;color;s
[1] ⍝ Arguments:
[2] ⍝ [1] Title (required, use '' for blank)
[3] ⍝ [2] Period (default 2×pi)
[4] ⍝ [3] Amplitude (default 1)
[5] ⍝ [4] Vertical minimum (default ¯1)
[6] ⍝ [5] Vertical maximum (default 1)
[7] ⍝ [6] Vertical offset (default 0)
[8] ⍝ [7] Horizontal minimum (default 0)
[9] ⍝ [8] Horizontal maximum (default 2×pi)
[10] ⍝ [9] Horizontal offset (default 0)
[11] ⍝ [10] Displayed axes: 'x'; 'y'; 'xy' or 'yx' for both; '' for neither
[12] ⍝ (default 'xy')
[13] ⍝ [11] Tick interval: x,y; 0 for do not display (default (0.2,0.2×pi)
[14] ⍝ [12] Colors: bg rgb triple, fg rgb triple (default (0 0 0)(255 255 255))
[15] ⍝ Use ⍬ for default value. Trailing arguments to be defaulted may be elided.
[16] ⍝
[17] :IF 82=⎕DR args ⋄ args←,⊂args ⋄ :END ⍝ In case title was the only argument
[18] args←12↑args,12⍴⊂⍬
[19] (s/args)←(s←args≡¨⊂⍬)/'' (○2) 1 ¯1 1 0 0 (○2) 0 'xy' (.2,○.2)
((0 0 0)(255 255 255))
[20] (title per ampl vmin vmax voff hmin hmax hoff axes ticks color)←args
[21] ⍝ Processing...
∇
In this version, the user can type:
This produces the same result as does the previous example.
Notice that, as in Example2, the code permits the user to give the first argument as ⍬, which will set the title to '', but the documentation in the function has been deliberately modified to conceal this. If we were to allow the user to supply a placeholder for any argument, and then allow him to omit trailing placeholders, he would be entitled to assume that he could type:
to run the function with all of the arguments taking on their default values. In reality, of course, doing so would raise a syntax error, as APL does not permit us to define functions which are ambivalently either niladic or monadic. A user who really wants to default all of the arguments must nevertheless supply either a value or a placeholder for (only) the first one, which is counter-intuitive. Rather than document this as a special-case exception, it is more readily understandable to simply tell the user that the first argument must be supplied. It helps if the first argument has an easy and obvious (and documented!) "logical default" value, such as a blank title.
All of the default values in Example2 and Example3 are assigned "simultaneously", i.e. in a single statement. This is easy enough, but may be unrealistic. Sometimes the best (or only appropriate) default value for a particular argument will not be a constant, but rather some function of the other arguments. In that case, we will need to assign default values individually. If the computation of these default values from some set of other argument values is trivial, we can simply replace the statement in Example3[19] with twelve individual assignment statements. Sometimes, however, a suitable default may be the result of a complex calculation that we need to do to get the result, and so we’d like to set the default as a function of some intermediate result of some prior calculation. In that case, we would want to scatter the individual assignments of the defaults throughout the function.
When we do that, we wind up with a function that looks something like:
∇ Example4 args;title;per;ampl;vmin;vmax;voff;hmin;hmax;hoff;axes;ticks;color
[1] ⍝ Arguments:
[2] ⍝ [1] Title (required, use '' for blank)
[3] ⍝ [2] Period (default 2×pi)
[4] ⍝ [3] Amplitude (default 1)
[5] ⍝ [4] Vertical minimum (default -arg[3])
[6] ⍝ [5] Vertical maximum (default arg[3])
[7] ⍝ [6] Vertical offset (default 0)
[8] ⍝ [7] Horizontal minimum (default 0)
[9] ⍝ [8] Horizontal maximum (default arg[2])
[10] ⍝ [9] Horizontal offset (default 0)
[11] ⍝ [10] Displayed axes: 'x'; 'y'; 'xy' or 'yx' for both; '' for neither
[12] ⍝ (default 'xy')
[13] ⍝ [11] Tick interval: x,y; 0 for do not display (default (0.2×arg[3]),
[14] ⍝ 0.1×arg[2])
[15] ⍝ [12] Colors: bg rgb triple, fg rgb triple (default (0 0 0)(255 255 255))
[16] ⍝ Use ⍬ for default value. Trailing arguments to be defaulted may be elided.
[17] ⍝
[18] :IF 82=⎕DR args ⋄ args←,⊂args ⋄ :END ⍝ In case title was the only argument
[19] args←12↑args,12⍴⊂⍬
[20] (title per ampl vmin vmax voff hmin hmax hoff axes ticks color)←args
[21] :IF title≡⍬ ⋄ title←'' ⋄ :END
[22] :IF per≡⍬ ⋄ per←○2 ⋄ :END
[23] :IF ampl≡⍬ ⋄ ampl←1 ⋄ :END
[24] ⍝ Processing...
[25] :IF vmin≡⍬ ⋄ vmin←-ampl ⋄ :END
[26] :IF vmax≡⍬ ⋄ vmax←ampl ⋄ :END
[27] :IF voff≡⍬ ⋄ voff←0 ⋄ :END
[28] ⍝ Processing...
[29] :IF hmin≡⍬ ⋄ hmin←0 ⋄ :END
[30] :IF hmax≡⍬ ⋄ hmax←per ⋄ :END
[31] :IF hoff≡⍬ ⋄ hoff←0 ⋄ :END
[32] ⍝ Processing...
[33] :IF axes≡⍬ ⋄ axes←'xy' ⋄ :END
[34] :IF ticks≡⍬ ⋄ ticks←(.2×ampl),.1×per ⋄ :END
[35] :IF color≡⍬ ⋄ color←(0 0 0) (255 255 255) ⋄ :END
[36] ⍝ Processing...
∇
We could mix these techniques, assigning constant defaults in one statement, "easy" calculated defaults in separate statements at the top of the function, and complex calcuated defaults as the intermediate results become available, but our function will be much easier to read and follow if we handle all of the defaults the same way. In addition, we make the function somewhat easier to follow by putting the setting of the default values for the arguments near the sections of code where the arguments first get used, so the reader doesn’t have to refer back to the top of the function repeatedly to see what they are. These refinements provide us with a lot more flexibility in choosing default values, but are transparent to the end user, who invokes Example4 identically to Example3:
Example4 is a significant improvement over Example1, but we can use the power of nested arrays and strand notation to do a lot better. In Example4, the user must still remember (or look up, each time) the positions of the particular elements of the argument strand that he is interested in, and must still type a placeholder ⍬ for each non-trailing argument he doesn’t care about. We can relieve him of these burdens as well. To do this, we must abandon the traditional view of formal arguments as collections of argument values.
To implement our final strategy, which we refer to as "named arguments", we assign each argument a meaningful name. Our user will no longer need to remember some set of meaningless index values that are arbitrarily associated with the arguments of interest to him. Instead, he need remember only the names. (If there are users out there who find remembering that amplitude and vertical maximum are in the 3rd and 5th positions respectively easier than remembering that amplitude and vertical maximum are called ‘ampl’ and ‘vmax’ – or ‘amplitude’ and ‘vertical_maximum’ – respectively, I’m not at all sure I want to meet them.) The formal arguments to the function are used to pass associations between names and values rather than just values. When our function needs to assign a value to a logical argument, it uses the associated name to select the appropriate value instead of using its position in the argument strand. The position becomes meaningless to the programmer (as it already was to the user), and the user can enter the arguments of interest to him in any order he chooses.
We can write a utility function that will take a vector of pairs of names and associated values and, for a given name, return the associated value if there is one, otherwise return a given default value. While that’s all we need, I have found it helpful for debugging purposes to have it also remove the name whose value is being extracted (along with its value) from the argument vector, so that the current value of the variable that represents the argument to the calling function contains only those logical arguments that have not yet been extracted. In addition, I find it helpful for it to incorporate a "meta-default" value, which can, among other uses, serve as a flag to indicate that a required argument has been omitted, so that the lack of an explicitly passed default value lets the reader of the calling function know that an explicit value is expected. The following function does all of this:
∇ R←ad ArgValue args;arg;default;s
[1] ⍝ Separates the value associated with a name from a vector of name-val pairs.
[2] ⍝ args is a vector of name-value pairs. Names must be character scalars
[3] ⍝ or vectors.
[4] ⍝ ad[1] is the name for which the associated value will be separated.
[5] ⍝ ad[2] (optional) is the value to be returned if ad[1] does not appear as
[6] ⍝ a name in args (default ⍬).
[7] ⍝ R[1] is the value associated with ad[1], or its default (ad[2] or ⍬).
[8] ⍝ R[2] is args with the pair corresponding to ad[1] removed.
[9] ⍝
[10] :IF ~(⎕DR ad)∊326 807 ⋄ ad←⊂ad ⋄ :END ⍝ ad may be an unenclosed name
[11] (arg default)←2↑ad,⊂⍬ ⍝ Supply default if not given
[12] :IF ∨/s←(↑¨args)∊⊂arg ⍝ Name is in args
[13] R←(2⊃↑s/args) ((~s)/args) ⍝ Return (value) (remaining elements of args)
[14] :ELSE ⍝ Name is not in args
[15] R←default args ⍝ Return (default value) (all elements of args)
[16] :ENDIF
∇
Using this function, we can rewrite Example4 as:
∇ Example5 args;title;per;ampl;vmin;vmax;voff;hmin;hmax;hoff;axes;ticks;color
[1] ⍝ Argument names:
[2] ⍝ 'title': Title (required, use '' for blank)
[3] ⍝ 'per': Period (default 2×pi)
[4] ⍝ 'ampl': Amplitude (default 1)
[5] ⍝ 'vmin': Vertical minimum (default -arg[3])
[6] ⍝ 'vmax': Vertical maximum (default arg[3])
[7] ⍝ 'voff': Vertical offset (default 0)
[8] ⍝ 'vmin': Horizontal minimum (default 0)
[9] ⍝ 'vmax': Horizontal maximum (default arg[2])
[10] ⍝ 'voff': Horizontal offset (default 0)
[11] ⍝ 'axes': Displayed axes: 'x'; 'y'; 'xy' or 'yx' for both; '' for neither
[12] ⍝ (default 'xy')
[13] ⍝ 'ticks': Tick interval: x,y; 0 for do not display (default 0.2×arg[3]),
[14] ⍝ 0.1×arg[2])
[15] ⍝ 'color': Colors: bg rgb trpl, fg rgb trpl (default (0 0 0)(255 255 255))
[16] ⍝
[17] :IF 82=⎕DR ↑args ⋄ args←,⊂args ⋄ :END
[18] (title args)←'title' '' ArgValue args
[19] (per args)←'per' (○2) ArgValue args
[20] (ampl args)←'ampl' 1 ArgValue args
[21] ⍝ Processing...
[22] (vmin args)←'vmin' (-ampl) ArgValue args
[23] (vmax args)←'vmax' ampl ArgValue args
[24] (voff args)←'voff' 0 ArgValue args
[25] ⍝ Processing...
[26] (hmin args)←'hmin' 0 ArgValue args
[27] (hmax args)←'hmax' per ArgValue args
[28] (hoff args)←'hoff' 0 ArgValue args
[29] ⍝ Processing...
[30] (axes args)←'axes' 'xy' ArgValue args
[31] (ticks args)←'ticks' ((.2×ampl),.1×per) ArgValue args
[32] color←↑'color' ((0 0 0) (255 255 255)) ArgValue args
[33] ⍝ Processing...
∇
Now the user who only cares about the title, amplitude, vertical minimum and vertical maximum, as in Examples 2 through 4, can type:
or:
or any other permutation of the arguments; position in the strand has become meaningless. He need not know what the arguments of no interest to him are, how many there are, or even that they exist.
Note the slight bit of additional user-friendliness provided by Example5[17], which allows the user who only cares about one argument (and presumably has no idea what a "strand" is) to type, say:
which is consistent with what he types when supplying more than one argument, rather than:
which is noticeably different from the more general case, and looks rather odd.
The next time you write a function that needs to take multiple arguments, some of which users are likely to want to allow to take on default values, consider using named arguments. You’ll be making your users’ lives easier.