Skip to content

Utility Corner: Polyadic Functions

Vector 18:2, page 38

programming techniques

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

Here is another Zark newsletter extract. As always, I hope you are all still enjoying reading these reprints. I certainly enjoy reading through the old issues of Zark in order to choose these extracts. Once again, can I remind you all that although these are reprints, we still expect submissions to the challenges set, as there is always a chance that new light can be shed on these old problems. We particularly encourage submissions in the more recent variants of APL, for example J and K.

Utility Corner: Polyadic Functions

(The purpose of this column is to make you more productive by introducing you to utility functions. Think of utility functions as APL functions that have names instead of symbols. By expanding your function vocabulary, you’ll be able to write APL code that’s more concise, more efficient, and more readable.)

In last issue’s LIMBERING UP column, we asked you to write a function named MORTGAGE that returns the monthly repayment amount for any specified interest rate, loan amount, and term. For example, if the interest rate is .09, the load amount is 90000, and the term is 30 years, the monthly repayment amount is 724.16.

To save you some time, we provided the formula:

V←÷1+INT÷12 ⍝ Monthly discount rate
PMT←PRIN÷(V-V*1+12×TERM)÷1-V
INT: Nominal annual interest rate
PRIN: Principal (amount borrowed)
TERM: Number of years of load
PMT: Monthly payment

J. Bruce King let us know that the formula can be algebraically simplified to:

IM←INT÷12
TM←TERM×12
PMT←PRIN×IM÷1-(1+IM)*-TM

We’ll use Bruce’s formula below.

At first glance, the MORTGAGE function seems trivial:

    ∇ PMT←MORTGAGE PARAMS;IM;TM
[1]   ⍝ Return monthly repayment amount PMT
[2]   ⍝ from interest PARAMS[1], principal
[3]   ⍝ PARAMS[2], and term PARAMS[3]
[4]    IM←PARAMS[1]÷12
[5]    TM←PARAMS[3]×12
[6]    PMT←PARAMS[2]×IM÷1-(1+IM)*-TM
    ∇

This function assumes each input item is a scalar. The problem becomes more challenging if you allow the input items to be vectors or matrices. For example, suppose interest is 8%, 9%, 10%; principal is 90000; term is 20, 25, 30. Two questions arise:

  1. How can these three arrays (.08 .09 .10 and 90000 and 20 25 30) be jammed into one or two arguments?
  2. Should the result be a three-element vector or a three-by-three matrix?

It’s the former if the input arrays are assumed to be scalar conformable; this is, each array has the same shape or is a singleton – the elements are in one-to-one correspondence:

Scenario 1:

Principal  =   90000   "    "
Interest   =     .08  .09  .10
Payment    =       ?    ?    ?

It’s the latter, a matrix, if each element of the input arrays is considered with every possible combination of the elements of the other input arrays:

Scenario 2:

Principal  =   90000
Interest   =           .08  .09  .10
                    ┌────┬────┬────┐
                 20 │  ? │  ? │  ? │
Term       =     25 │  ? │  ? │  ? │
                 30 │  ? │  ? │  ? │
                    └────┴────┴────┘

In Scenario 1, the shape of the result is ⍴(INT+PRIN+TERM). In Scenario 2, the shape is ⍴(INT∘.+PRIN∘.+TERM).

Let’s deal first with the question of passing three arguments to the MORTGAGE functions. Here are the suggestions we received:

  1. 3 1 3 MORTGAGE .08 .09 .10 90000 20 25 30

    This syntax, from J Bruce King, passes all three input arrays in the right argument as a simple vector. The arrays are ravelled and catenated. The left argument is a three-element vector of the lengths of each ravelled input array. If omitted, the left argument is assumed to be 1 1 1, permitting the syntax, MORTGAGE .09 90000 30.

  2. MORTGAGE (.08 .09 .10) (90000) (20 25 30)

    Fans of vector (strand) notation, Jack H Zinn and Rick Gracey fused the three input arrays into a three-item nested vector argument. The first line of the MORTGAGE function converts the nested array back into three simple arrays:

        ∇ PMT←MORTGAGE PARAMS;INT;PRIN;TERM
    [1]    (INT PRIN TERM)←PARAMS  ⍝ APL2
    [1]    INT PRIN TERM←PARAMS    ⍝ APL*PLUS
    
  3. INT←.08 .09 .10 ⋄ PRIN←90000 ⋄ TERM← 20 25 30 ⋄ MORTGAGE

    Ignoring argument altogether, John E. Lauer suggested the expedient of passing the three input arrays as global variables. (Diamond ⋄ is a statement separator in APL*PLUS.)

  4. MORTGAGE 'INT←.08 .09 .10 ⋄ PRIN←90000 ⋄ TERM← 20 25 30'

    Not to be outdone by himself, John E. Lauer then suggested passing an executable character vector as the argument. By localizing these variables within MORTGAGE and by executing the character vector at the start of MORTGAGE, the input arrays are passed without ever existing in the global environment.

  5. MORTGAGE .08 .09 .10,90000,[1.5]20 25 30

    This syntax, from Nick Rago, permits the right argument to have any shape, as long as its last dimension is 3. In this case, the argument has shape 3 3:

    0.08 90000 20
    0.09 90000 25
    0.1  90000 30
    

    The shape of the result is the same as the shape of the right argument, less its last element.

Let’s deal now with the second question, that of deciding whether the input arrays are to be treated in a scalar conformable fashion (INT+PRIN+TERM) or in an outer product fashion (INT∘.+PRIN∘.+TERM).

Some respondents wrote logic that implemented the outer product design (all possible combinations) and some wrote logic that was a hybrid of both scalar conformable and outer product designs. In the hybrid functions, the input arrays with the same shape were used in one-to-one (scalar) correspondence, and all others were applied in outer product fashion. For example, given the shape of INT is 3, the shape of PRIN is 3, and the shape of TERM is 4, the outer product functions return a result with shape 3 3 4, and the hybrid functions return a result with shape 3 4.

The following function is typical of those using a strictly outer product approach (from King and Zinn):

    ∇ PMT←MORTGAGE1 PARAMS;A;B;C;IM;PRN;TM
[1]   ⍝ Return monthly repayment amount PMT
[2]   ⍝ from interest 1⊃PARAMS, principal
[3]   ⍝ 2⊃PARAMS, and term 3⊃PARAMS. Result
[4]   ⍝ has shape ⊃,/⍴¨PARAMS.
[5]    A←⍴⍴IM←(1⊃PARAMS)÷12 ⍝ Monthly int.
[6]    B←⍴⍴PRN←2⊃PARAMS ⍝ Principle amount
[7]    C←⍴TM←(3⊃PARAMS)×12 ⍝ Term in months
[8]    PMT←(⍋(A,B,⍴C)/2 1 3)⍉PRN∘.×(IM∘.+C⍴0)÷1-(1+IM)∘.*-TM
    ∇

The hybrid functions were well-written but struck us as somewhat ill-behaved. Notice the shape of the result (PMT) as the shapes of the input arrays vary.

⍴INT ⍴PRIN ⍴TERM ⍴PMT
1 2 3 1 2 3
2 2 3 2 3
3 2 3 3 2
4 2 3 4 2 3
5 2 3 5 2 3

(In defence of the hybrid fans, we deliberately misled them by posing problems that required both scalar conformable and outer product interpretations.)

The design of Nick Rago’s function was unique, in that the nature of the right argument eliminates the scalar conformable vs. outer product question.

The shape of the argument defines the shape of the result:

    ∇ PMT←MORTGAGE2 PARAMS;IM;TM
[1]   ⍝ Return monthly repayment amount PMT
[2]   ⍝ from interest (1 0 0/PARAMS),
[3]   ⍝ principible (0 1 0/PARAMS), and term
[4]   ⍝ (0 0 1/PARAMS). Result has shape
[5]   ⍝ ¯1↓⍴PARAMS
[6]    IM←(1 0 0/PARAMS)÷12 ⍝ Monthly int.
[7]    TM←(0 0 1/PARAMS)×12 ⍝ Term in mons
[8]    PMT←(¯1↓⍴PARAMS)⍴(0 1 0/PARAMS)×IM÷1-(1+IM)*-TM
    ∇

Given these two functions, here are the solutions to the problems presented in the last issue of the news:

  1. Interest is 9%; principal is 90000; term is 30.

    MORTGAGE2 .09 90000 30
    MORTGAGE1 .09 90000 30
    
  2. Interest is 9%; principal is 90000; term is 15, 20, 25, 30 (4-element vector result).

    MORTGAGE1 .09 90000(15 20 25 30)
    MORTGAGE2 .09,90000,[1.5]15 20 25 30
    
  3. Interest is 8%, 9%, 10%; principal is 90000; term is 20, 25, 30 (3-element vector result).

    1 1 ⍉ MORTGAGE1 (.08 .09 .10) 90000 (20 25 30)
    MORTGAGE2 .08 .09 .10, 90000,[1.5] 20 25 30
    

    MORTGAGE1 computes all possible combinations and then extracts those desired (on the diagonal) via 1 1 ⍉

  4. Interest is 8%, 9%, 10%; principal is 90000; term is 15, 20, 25, 30 (3 by 4 matrix result).

    MORTGAGE1(.08 .09 .10) 90000 (15 20 25 30)
    MORTGAGE2(⍉4 3⍴.08 .09 0.10),90000,[2.5]3 4⍴15 20 25 30
    

    MORTGAGE2 uses reshape, laminate, and transpose to construct the needed three-dimensional argument.

[Ed:] In APL2 the last example above could have been written as:

      MORTGAGE2 ⊃.08 .09 .10∘.,90000∘.,15 20 25 30