Utility Corner: Execute Inverse
Vector 20:3, page 25
Transcribed from the printed issue; not yet reviewed. Read it in the PDF of the issue, page 25
Here is another Zark newsletter extract. As always, don’t hesitate to send in any solutions you may have that you may be inspired to investigate. We have had some interesting responses in recent issues which prove that however old the problem new approaches are always worth considering.
Utility Corner: Execute Inverse¶
(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, you were asked to define a monadic utility function EI (Execute Inverse) that converts any array A to a character vector such that A≡⍎EI A. For example:
The ability to convert any array to a simple character vector has some useful applications. For example, suppose your workspace contains a function named MODEL that requires a global variable named TABLE. Suppose you want to incorporate the assignment and localisation of TABLE into the MODEL function so that MODEL becomes a self-contained function, requiring no global variables. TABLE will exist only while MODEL is running. Finally, suppose line [17] of MODEL is the line on which you want TABLE to be assigned. Here’s how you can use EI in APL*PLUS to eliminate TABLE as a global variable:
⎕DEFL 'MODEL[17] TABLE←',EI TABLE- Localise
TABLEin line[0]ofMODEL - Erase
TABLEfrom the workspace
The system function ⎕DEFL (DEFine Line) modifies a single line of a function when given a character vector in the format
A second application of EI is to transfer any arbitrary array from one APL system to another. Here are the steps:
-
On the originating system,
-
Transmit the character vector to the recipient system, via a transmission function such as
⎕ARBIN(APL*PLUS), or by writing it to an operating system file on the originating system, transferring it to an operating system file on the recipient system, and reading it into the active workspace on the recipient system. Take care to perform translations if the APL characters have different locations in the atomic vectors (⎕AV) of the originating and recipient systems. -
On the recipient system,
At first glance, EI seems an almost trivial function to write. Basically, you pass numbers through ⍕, and you enclose characters within a set of quotes. Hoever, the more you look at it, the more exceptions you realise have to be handled. Here is a list of some different cases:
| Example | |
|---|---|
| Numeric scalar | 5 |
| Empty numeric vector | ⍳0 |
| One-element numeric vector | ,5 |
| Numeric vector | 2 3 4 |
| Empty numeric array | 0 3⍴0 |
| Numeric array | 2 1⍴6 7 |
| Character scalar | 'A' |
| Empty character vector | '' |
| One-element character vector | ,'A' |
| Character vector | 'ABC' |
| … with embedded quotes | 'CAN''T' |
| … with nondisplay elements | 'AB',⎕TCNL,'C' |
| Empty character array | 0 3⍴'' |
| Character array | 2 1⍴'AB' |
| Nested scalar | ⊂'ABC' |
| Empty nested array (with prototype) | 0 3⍴1 1⍴0 |
| Nested vector | 'ABC'(2 3)0 |
| Nested array | 1 3⍴'ABC'(2 3)0 |
| Repetitive numeric array | 50⍴0 |
| Repetitive character array | 50⍴' ' |
| Repetitive nested array | 50⍴⊂⍳0 |
Evan Jennings pointed out that APL2 contains a system function ⎕TF (Transfer Form) that performs this very task. Here is his submission:
The right argument of ⎕TF ('A') is the name of the array to be converted (in this case, the argument variable). The left argument (2) tells ⎕TF that the “extended” (executable) transfer form is desired. Since the result begins with the name of the array and an assignment arrow (A←), the 2↓ eliminates them.
Rex Swain indicated that the logic of the EI function depends on the implementation of APL on which it runs. He submitted two versions, one for conventional (non-nested) APL systems, and one for nested APL systems. Here is the conventional one:
∇ R←EI A;S;⎕PP
[1] ⍝ Execute Inverse: A≡⍎EI A
[2] ⍝ Returns character vector that can
[3] ⍝ be executed to re-crete the
[4] ⍝ array (not nested or hetero).
[5] ⍝
[6] S←⍴A ⍝ Record original shape
[7] A←,A ⍝ Then ravel
[8] ⍝ Need explicit reshape if rank≥2:
[9] R←(1<⍴S)/(⍕S),'⍴'
[10] ⍝ Need to ravel if a one-element vector:
[11] R←R,(S≡,1)/','
[12] →(0=1↑0⍴A)⍴L1 ⍝ Brance if numeric
[13] ⍝ Wrap/double quotes on characters:
[14] R←R,'''',((1+A='''')/A),''''
[15] →0
[16] ⍝ Use maximum print precision for
[17] ⍝ floating point numbers:
[18] L1:⎕PP←17
[19] ⍝ Format numbers; handle empty case:
[20] R←R,(((S=,0)/'⍳'),(0∊S)/'0'),⍕A
∇
The use of ⎕PP (Print Precision) is needed in the event that floating point numbers (such as those from 1÷3 7) are involved. The format function (⍕) is sensitive to the setting of ⎕PP, and normally formats numbers to just 10 decimal places. To avoid loss of precision, ⎕PP is set to its largest (most decimal places) setting. On some versions of APL, the largest setting is 16.
Here is Rex’s version that supports nested and heterogeneous arrays. Heterogeneous arrays are arrays that are not nested but that contain both numbers and characters. (We modified Rex’s function somewhat to work in other nested versions of APL than just Dyalog APL, such as the original “Evolution Level 1” version of nested APL*PLUS.)
∇ R←EI A;B;C;D;E;I;J;K;L;N;S;T;U;X;⎕IO;⎕PP
[1] ⍝ Execute Inverse: A≡⍎EI A
[2] ⍝ Returns character vector that can
[3] ⍝ be executed to re-crete the
[4] ⍝ array. Recursive
[5] ⍝
[6] ⎕IO←1
[7] K←⍴S←⍴A ⍝ Original shape and rank
[8] A←,A ⍝ Then ravel
[9] ⍝ Branch if fewer than 2 elements
[10] →(2>⍴A)/L1
[11] ⍝ ... or if not all the same:
[12] →(A≡(⍴A)⍴A[1])↓L1
[13] ⍝ Else express as S⍴A[1]:
[14] R←(⍕S),'⍴',EI A[1]
[15] →0
[16] L1:N←1<≡A ⍝ Nested?
[17] ⍝ Need explicit reshape if rank≥2 or
[18] ⍝ if nested empty:
[19] R←((K>1)∨N∧0∊S)/(⍕S),'⍴'
[20] ⍝ Need ravel if one-element vector:
[21] R←R,(S≡,1)/','
[22] →N/L4 ⍝ Branch if nested ...
[23] →(∧/0 ' '∊1⊃1↑0⍴⊂A)/L4 ⍝ or hetero
[24] →(' '=1↑0⍴A)/L2 ⍝ or character
[25] ⍝ Else numeric; use maximum print
[26] ⍝ precision for floating point no.s:
[27] ⎕PP←16
[28] ⍝ Format numbers; handle empty case:
[29] R←R,(((S≡,0)/'⍳'),(0∊S)/'0'),⍕A
[30] →0
[31] ⍝ Wrap/double quotes on characters:
[32] L2:X←'''',((1+A='''')/A),''''
[33] ⍝ Branch if no terminal ctrl chars
[34] ⍝ (Use ⎕TC[⎕IO+i] of ⎕AV[⎕IO+i] for
[35] ⍝ non-APL*PLUS versions of APL.)
[36] T←⎕TCBS,⎕TCNL,⎕TCLF,⎕TCFF,⎕TCHT,⎕TCESC,⎕TCBEL,⎕TCDEL,⎕TCNUL
[37] →(1∊U←X∊T)↓L3
[38] I←U/⍳⍴U ⍝ Indices of these chars
[39] D←T⍳X[I] ⍝ Which one is each?
[40] C←''',⎕TCBS,'''',⎕TCNL,'''',⎕TCLF,'''',⎕TCFF,'''',⎕TCHT,'''',⎕TCESC,'''',⎕TCBEL,'''',⎕TCDEL,'''',⎕TCNUL,'''
[41] ⍝ Lengths and beginnings:
[42] L←(9 9 9 9 9 10 10 10 10)[D]
[43] B←(0 9 18 27 36 45 55 65 75)[D]
[44] ⍝ Shorten if not after normal char:
[45] L←L-T←(2,U)[I]
[46] B←B+T
[47] ⍝ ... or before normal char:
[48] L←L-2×(1↓U,1)[I]
[49] E←(⍴X)⍴1 ⍝ Expansion (repl) vector
[50] E[I]←L
[51] X←E/X ⍝ Expand it
[52] ⍝ Adjust inds for expanded version:
[53] I←(+\1,E)[I]
[54] ⍝ J←(⍳L[1]),(⍳L[2]),(⍳L[3]),... :
[55] J←L/¯1↓0,+\L
[56] J←(⍳⍴J)-J
[57] ⍝ Stuff them in:
[58] X[J+L/I-1]←C[J+L/B]
[59] ⍝ Drop 1st/last 3 chars if needed:
[60] X←(3×0 1≡2↑U)↓(¯3×1 0≡¯2↑U)↓X
[61] L3:R←R,X
[62] →0
[63] ⍝ Nested or hetero; branch if more
[64] ⍝ than one item
[65] L4:→(1<S←⍴A)/L5
[66] ⍝ Use 1↑ to get prototype if empty:
[67] R←R,'⊂',EI 1⊃1↑A
[68] →0
[69] ⍝ Nested array with >1 item. One
[70] ⍝ algorithm is:
[71] ⍝ R←R,⊃,/'(',¨(EI¨A),¨')'
[72] ⍝ but it gives more parentheses than
[73] ⍝ needed, so we loop instead:
[74] L5:I←0
[75] L6:I←I+1
[76] →(I>S)/0
[77] ⍝ Get item and convert it:
[78] U←EI D←I⊃A
[79] ⍝ Need parentheses if ...
[80] →(1<≡D)/L7 ⍝ Nested
[81] →(∧/0 ' '∊1⊃1↑0⍴⊂D)/L7 ⍝ or hetero
[82] ⍝ ...or if numeric nonscalar:
[83] K←⍴⍴D
[84] →((0=1↑0⍴D)∧(K≠0),1)/L7,L8
[85] ⍝ But not if a char scalar:
[86] →(K=0)/L8
[87] ⍝ Need parentheses if rank >1 or if
[88] ⍝ a one-element vector:
[89] →((K>1)∨1∊⍴D)/L7
[90] ⍝ or if some terminal ctrl chars
[91] →(1∊D∊⎕TCBS,⎕TCNL,⎕TCLF,⎕TCFF,⎕TCHT,⎕TCESC,⎕TCBEL,⎕TCDEL,⎕TCNUL)↓L8
[92] L7:U←'(',U,')' ⍝ Provide the parens
[93] ⍝ Incl space betw items if needed:
[94] L8:T←('('=1↑U)∨(¯1↑R)∊' )'
[95] R←R,(T↓' '),U
[96] →L6
∇
Lines [9] to [15] handle arrays whose items are all the same. Because of this logic, the result can be much smaller. For example, if the argument is a matrix of 500 zeros the result is '50 10⍴0' instead of '50 10⍴0 0 0 0 ... 0'.
To handle nested arrays, the function recurses (calls itself) to format the items of the array. See lines [14], [67], and [78].
To handle terminal control characters, such as new lines (carriage returns), linefeeds, formfeeds, and so on, the function replaces those characters by system functions that return them. See lines [33] to [60]. The following illustrates this replacement logic. Suppose the right argument of the EI function is ⎕TCFF,'ABC',⎕TCNL,'DE',⎕TCNL,⎕TCLF,'GE',⎕TCESC. Here’s what happens:
[36-37] Branch if no terminal chars
[32] X: '_ABC_DE__GE_'
[37] U: 01000100110010
[38] I: 2 6 9 10 13
[39] D: 4 2 2 3 6
[42] L: 9 9 9 9 10
[43] B: 27 9 9 18 45
[45] L: 9 9 9 8 10
[46] B: 27 9 9 19 45
[48] L: 9 9 7 8 10
[49-50] E: 1 9 1 1 1 9 1 1 7 8 1 1 10 1
[51] X: '_________ABC_________DE_______________GE__________'
[53] I: 2 14 25 32 42
[55-56] J: 1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9 10
[58] X: '',⎕TCFF,'ABC',⎕TCNL,'DE',⎕TCNL,⎕TCLF,
'GE',⎕TCESC,''
[60] X: ⎕TCFF,'ABC',⎕TCNL,'DE',⎕TCNL,⎕TCLF,'GE',⎕TCESC
If your version of APL is not APL*PLUS use the terminal control system functions available (e.g. ⎕TC and ⎕AV). For example, make the following modifications:
[36] T←⎕TC,⎕AV[1]
[40] C←''',⎕TC[⎕IO],'''',⎕TC[⎕IO+1],'''',⎕TC[⎕IO+2],'''',⎕AV[⎕IO],'''
[41] ⍝ Lengths and beginnings:
[42] L←(12 14 14 12)[D]
[43] B←(0 12 26 40)[D]
On line [66] of the above EI function, the word “prototype” is used. The “prototype” of an empty array is the array return when you “pad” it via take (1↑A) or expansion (0\A). For simple empty arrays such as ⍳0 or '', the prototype is obvious. You get back 0 when do 1↑⍳0 and get back a blank when you do 1↑''.
However, for an empty nested array, what is the prototype? Zero? Blank? Neither?
The prototype depends on how the empty nested array was created. For example, the prototype of 0⍴(3 4⍴⍳12) 'HELLO' is 3 4⍴0. The prototype of 0⍴'HELLO' (3 4⍴⍳12) is 5⍴' '. In general, the prototype is the same as the first item of the array from which the empty array is derived, with all numbers replaced by zeros and all characters replaced by blanks.
It is the desire to accurately reproduce the prototype of the empty nested array that accounts for the logic on line [67]: R←R,'⊂',EI 1⊃1↑A.
1⊃1↑A returns the prototype, EI formats it, '⊂' encloses it, and R (e.g. '0⍴') reshapes it back to its empty shape.
Limbering Up: Word Wrap¶
(The purpose of this column is to work some flab off your APL midsection. Like muscles, your APL skills can atrophy if not exercised with adequate frequency and variety. This column presents a task for you to perform. Set aside a few minutes from your busy schedule and work the task. Mail in your solution and stay tuned for the results.)
Given a lengthy piece of text, it is sometimes necessary to “wrap” it so it fits within a specified width. The piece of text is broken into subsets, without breaking any words, so that each subset is no longer than the specified width. The following sample has been wrapped within a width of 17:
Your task is to write a dyadic function WRAP that does wrapping. Its right argument is the character vector to be wrapped. Its left argument is the width within which the text is to be wrapped. Its result is a character vector with new-line (carriage return) characters inserted in such a way that the text wraps appropriately when displayed. For example:
Some notes:
- The result has the same length as the right argument. The newline character (
⎕TCNLor⎕TC[2]) replaces the first blank after the last word in each line. - If the right argument already contains newline characters, don’t ignore them and don’t remove them. Each one begins a new line. In a sense, existing new lines are “paragraph” delimiters.
- Consider each group of contiguous non-blank characters a word. The aim is to wrap the text without breaking any words. However, if any “word” is longer than the specified width, you should insert one or more newline characters within it to break it into pieces whose length equals the width. If this is the case the result will naturally be longer than the right argument.
Your primary objective is to write a function that works. You’ll receive extra credit (a pat on the back) if the function is efficient, clever, simple, elegant, or can leap tall buildings in a single bound. Send your solution to:
Vector Production, Brook House, Gilling East, York YO62 4JJ
Email: apl385@compuserve.com
The notable functions and their authors’ names will be printed in the next issue of Vector. Good luck and happy limbering.
Reprinted with kind permission from Zark APL Tutor News, a quarterly publication of Zark Incorporated, 23 Ketchbrook Lane, Ellington, CT06029, USA