Skip to content

DOIF Considered Harmful

Vector 9:1, page 133

development practice programming techniques

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

Introduction

From time to time attempts have been made to add structure to APL code through the use of defined functions. The IF function is probably the most widely accepted and successful of these attempts, allowing us to write branch statements in the form:

→label IF condition

In the last year or so, I have come across another function in the same vein - DOIF. DOIF can be used in the situation where you wish to execute some code only if a condition is true, for example to assign a value to the elided left argument of an ambivalent function:

[..]  →DOIF 0=⎕NC 'left' ⋄ left←....

To read this line of code, simply ignore the branch arrow the rest follows relatively easily.

To understand how DOIF works. If the condition on the right of DOIF evaluates to 1, an empty vector is returned. No branch occurs and execution continues with the statement following the diamond. If the condition evaluates to 0, the line number of the next line (⎕LC+1) is returned. A branch to the next line is executed; statements after the diamond are ignored.

The Culprit

Here is a listing of a typical DOIF function:

    ∇ R←DOIF W
[1]   ⍝ FN: Return next line number in CALLING fn if <~W>.
[2]   ⍝ W - Boolean scalar                Evaluated condition
[3]   ⍝ R - Numeric vector                Next line number or empty
[4]    R←(~W)/1+¯1↑2↑⎕LC
    ∇

What criticisms can we make about DOIF and its use? I want to concentrate on three specific areas:

  • use of statement separators
  • use of computed goto
  • poor APL style

Diamonds are Forever?

The DOIF function relies on the diamond statement separator. Without this only a single statement can be placed on a line of code; no statements can follow the DOIF itself. Not all versions of APL support statement separators. By encouraging their use (through the use of DOIF), portability of code is being impaired. Equally programmers who are not familiar with statement separators will be hard put to read the line of code and understand what it is intended to do.

We can further criticize the use of the diamond separator. Consider a situation where several statements must be executed if our condition is true. Each of these must be present on the same line of code — the natural result is a tendency to produce very long lines of code with resulting reductions in readability and ease of comprehension.

The use of comments will also be affected. Only one comment can be used per line of code restricting our description of an entire "block" of code to a single in-line comment. Of course we are free to use leading and trailing full-line comments but these can lack the immediacy and impact of comments at the end of a line of code.

Poor Quality Diamonds

It can be argued that the excessive use of diamondized lines of code leads to the creation of poor quality code. The statement separator creates within APL a new form of structure — the line of code is elevated to the lofty position of "module". Traditionally, the only form of module supported by APL has been the defined function (and defined operators in some APLs). Unfortunately, the only characteristic that a diamondized line shares with the defined function is the ability to cram in a lot of processing.

If we look at the two standard measures of module quality [see Page-Jones, Myers, Yourdon] — Coupling and Strength (=cohesion), the diamondized line will do pretty poorly. The only form of coupling (i.e. links to/from other modules) that can exist between lines of code is COMMON coupling. Common coupling is jargon for the use of "global" variables — if you think about it, there is no way to make a variable local to a line of code.

Similarly, in terms of strength it is unlikely that a single diamondized line will embody the whole of an APL function (indeed the use of DOIF precludes this possibility). Thus a "sequential strength" module is the best we can hope for when using diamondized lines. Without the use of statement separators these considerations would not be relevant. A line of code is not normally considered to be a module so the coupling and strength measures would not apply. It is the over-use of statement separators changing a simple line of code into a module which makes these concerns important.

The precise deficiencies of "sequential strength" or "common coupling" are not important, the simple fact is that the use of diamondized lines as modules of code can easily reduce the quality of the code we write. Even if this trap is avoided, more expertise and care is required to ensure the continued creation of high quality code. Expertise is always at a premium.

No More Automation!

Diamonds also reduce the opportunities for "automated" coverage testing. The built in monitor facilities found in some implementations of APL can be used to examine the coverage of a test, since they indicate which lines of code have been executed. The coverage of the monitor facilities is by line of code rather than statement. However, as long as a diamondized line of code involves no branch statements then we can rely on all the statements on the line being executed if the line is executed at all. If a branch is included, then some of the statements on the line may have been executed while others are not. DOIF always introduces such a branch.

Note that this argument is one aspect of a more general point — the use of diamondized lines of code may make it significantly more difficult to detect CPU hot-spots by blurring the CPU usage of several statements into a single figure. With well written code, we always have the option of removing the diamonds and re-executing the statements as individual lines of code. Unfortunately because of the computed branch generated by DOIF, this option is lost to us.

Computed Goto

The DOIF function computes the destination of the branch based on the value returned by the right argument. Of course, the language purists can argue that all conditional branch statements in APL are computed. The real point is that the destination of the branch is not fixed by a line label.

This can affect us in several ways. For example, when delivering the application code to the production environment, we may wish to remove comments and "diamondize" the code. This reduces the size of the delivered code (and possibly improves the performance of the code, especially in APL*PLUS/PC). At the very least, the speed of loading the code from workspace or file will be improved as a result of the reduction in volume of code.

Diamondization usually works by merging together unlabelled lines of code. The use of DOIF prevents the diamondization of code because of the branches to unlabelled lines. Without labels, the statements are merged together making it impossible to branch to the statement we require. Consider the following function which computes a simple average if the left argument is elided, or an N-period moving average if specified:

    ∇ R←A AVE W;iset;⎕IO
[1]   ⍝ FN: Average or moving average on vector W
[2]   ⍝ A - Numeric scalar                 Periodicity of average
[3]   ⍝ W - Numeric vector                 Data to average
[4]   ⍝ R - Numeric vector                 N-period moving average
[5]    ⎕IO←0                            ⍝ Simplifies index set
[6]    →DOIF 0=⎕nc 'A' ⋄ A←⍴W           ⍝ Default to simple average
[7]    iset←(⍳1+(⍴W)-A)∘.+⍳A            ⍝ Generate the index set
[8]    R←(+/W[iset])÷A                  ⍝ Sum over period and divide
[9]   ⍝                                    In APL2:   R←(A+/W)÷A
    ∇

      data←4 7 2 9 10 15 13 16
      AVE data                      ⍝ Simple average
9.5
      4 AVE data                    ⍝ 4 period moving average
5.5 7 9 11.75 13.5

The diamondized and decommented version of this function would look something like this:

    ∇ R←A AVE W;iset;⎕IO
[1]    ⎕IO←0 ⋄ →DOIF 0=⎕nc 'A' ⋄ A←⍴W ⋄ iset←(⍳1+(⍴W)-A)∘.+⍳A ⋄ R←(+/W[iset])÷A

When called dyadically, the branch statement will attempt to GOTO line 2, which is outside the scope of the function. Execution of the AVE function will terminate at this point without calculating a result; if an attempt is made to assign the result a VALUE ERROR will be reported.

APL Style

Style is, of course, a subjective thing. Even so, within the APL world there is a degree of consensus about what constitutes good style. Branching is often considered a necessary evil — "if you can avoid it do so" seems to be the argument. Such arguments are supported by measures of program complexity that are published from time-to-time which frequently assign the highest complexity ratings to "GOTOs".

The problem with functions like DOIF is that they encourage the use of branching even if it is not strictly necessary.

"Aha", reasons the programmer,

"I need to perform these calculations only if the following condition is true". More thinking quickly follows

"… we have a DOIF utility which makes this form of conditional execution so easy …".

In the blink of an eyelid, the following code is produced:

    ∇ ADJUSTED←TYPE PMT_ADJUST PAYMENT;adjustment;limit;taxamt;taxable
[1]   ⍝ FN: Adjust the payments if <TYPE> ....
[2]   ⍝ TYPE     - Numeric scalar          Type of payment
[3]   ⍝ PAYMENT  - Numeric vector          Vector of payments
[4]   ⍝ ADJUSTED - Numeric vector          Adjusted payments
[5]    adjustment←0                      ⍝ Assume no adjustment
[6]   ⍝                                    Adjustment calculation
[7]    →DOIF TYPE∊24 28 ⋄ limit←PMT_TAXLIMIT ⋄ taxable←PAYMENT>limit ⋄ taxamt←taxable×PMT_TAXRATE×PAYMENT-limit ⋄ adjustment←PMT_CHARGE+taxamt
[8]    ADJUSTED←PAYMENT-adjustment       ⍝ Adjust the payment
    ∇

The code is not well structured, for example the statements calculating the actual adjustment could easily be subsumed into a sub-function, so our code becomes:

    ∇ ADJUSTED←TYPE PMT_ADJUST PAYMENT;adjustment
[1]   ⍝ FN: Adjust the payments if <TYPE> ....
[2]   ⍝ TYPE     - Numeric scalar          Type of payment
[3]   ⍝ PAYMENT  - Numeric vector          Vector of payments
[4]   ⍝ ADJUSTED - Numeric vector          Adjusted payments
[5]    adjustment←0                      ⍝ Assume no adjustment
[6]   ⍝                                    Adjustment calc'n
[7]    →DOIF TYPE∊24 28 ⋄ adjustment←PMT_ADJ_CALC PAYMENT
[8]    ADJUSTED←PAYMENT-adjustment       ⍝ Adjust the payment
    ∇

Now the line involving DOIF is so simple, indeed the whole function is so short that perhaps we can remove the DOIF altogether:

    ∇ ADJUSTED←TYPE PMT_ADJUST PAYMENT;adjustment
[1]   ⍝ FN: Adjust the payments if <TYPE> ....
[2]   ⍝ TYPE     - Numeric scalar          Type of payment
[3]   ⍝ PAYMENT  - Numeric vector          Vector of payments
[4]   ⍝ ADJUSTED - Numeric vector          Adjusted payments
[5]    adjustment←0                      ⍝ Assume no adjustment
[6]    →none IF ~TYPE∊24 28              ⍝ Adjust if 24 or 28
[7]    adjustment←PMT_ADJ_CALC PAYMENT   ⍝ Adjustment calc'n
[8]   none:ADJUSTED←PAYMENT-adjustment   ⍝ Adjust the payment
    ∇

I would argue that the last listing is at least as readable as that involving DOIF, so we have lost nothing here. Indeed, we have gained many benefits by using an explicit label.

Flexibility of Design

We can, of course, go further and argue that the use of the branch is not a very flexible approach. If the specification changes so that the payment type is now a vector of potentially different types (one type per payment) we are presented with a difficult choice. We could call the function once for every single payment being made but performance may suffer as a result. Alternatively, the code can be modified to cope with the new specification.

Perhaps the following would have been a better solution to start with:

    ∇ ADJUSTED←TYPE PMT_ADJUST PAYMENT;adjustment;nonzero
[1]   ⍝ FN: Adjust the payments if <TYPE> ....
[2]   ⍝ TYPE     - Numeric scalar          Type of payment
[3]   ⍝ PAYMENT  - Numeric vector          Vector of payments
[4]   ⍝ ADJUSTED - Numeric vector          Adjusted payments
[5]    nonzero←TYPE∊24 28                ⍝ Nonzero adjustments
[6]   ⍝                                    Calculate adjustments
[7]    adjustment←nonzero×PMT_ADJ_CALC PAYMENT
[8]    ADJUSTED←PAYMENT-adjustment       ⍝ Adjust the payment
    ∇

No branching at all…

A frequently voiced maxim about designing and writing APL code suggests that it is important to make defined functions behave as much like primitives as possible (the "as possible" is crucial). Scalar extension is a fundamental principle within APL, and it is scalar extension that the branching solution makes much more difficult to achieve.

Conclusions

The problems surrounding the DOIF function originate from two aspects of its use:

  • its reliance on statement separators (diamonds)
  • the use of an unlabelled branch

The requirement to use statement separators means:

  • DOIF is not universal
  • very long lines of code can result
  • comments will probably be more sparse
  • comments will be less "immediate" (farther from the code)
  • it will be more difficult to ensure high quality code

The most important advantage claimed for the use of DOIF is an improvement in the readability of code. Even this seems a spurious claim because of the increased length of lines and the reduction in commenting.

The use of an unlabelled branch means we cannot change the statement structure of the function by, for example, splitting multi-statement lines so that:

  • opportunities for automatic coverage testing are reduced
  • detecting performance problem areas may be more difficult
  • "diamondization" for code delivery is much more difficult

DOIF alters the perceived complexity of writing a branching form of code rather than using "logical calculations" [McIntyre pp 566-567]. The programmer may as a result:

  • create inflexible designs
  • give insufficient thought to the modular design of code
  • generate additional branches which are not really needed
  • use branches when another technique would be more appropriate
  • increase the complexity of defined functions

These arguments do not just apply to the DOIF function. In general any function which attempts to introduce structured programming constructs, but which relies on unlabelled branching and/or the use of statement separators will suffer from the same problems.

To put it more simply, any utility function which relies on, or encourages, bad programming practices can only create more problems than it solves.

References

[1] McIntyre D B, Language as an Intellectual Tool: From Hieroglyphics to APL, IBM Systems Journal Vol 30 No 4 G321-0105-00

[2] Myers G J, Reliable Software Through Composite Design, Van Norstrand Reinhold 0-442-25620-6

[3] Page-Jones M, The Practical Guide to Structured Systems Design, Prentice Hall 0-13-690777-6

[4] Taylor A R, Private communications 1991, 1992,

[5] Yourdon, Modern Structured Analysis, Prentice Hall 0-13-598632-X