User:Cameron Chandoke/WIP/Functional Control Flow

From J Wiki
Jump to navigation Jump to search

Introduction / Motivation

All of J's control structures based on control words (e.g. while. do. try. catch.) have straightforward functional equivalents, which this article explores. They tend to make heavy use of gerunds for delayed evaluation ("quoting") of verbs.

The advantage of control-flow functions over control words, besides their being much shorter, is that they are phrases (part's' of a sentence) rather than sentences, and can thus be made part of a longer phrase or a sentence without requiring multiple sentences. In other words, they are easily composable. Hence they fit better with J's naturally functional style of programming.

In short:

  • Power (^:) provides if. and while.
  • Agenda (@.) provides elseif./else.
  • nested if-else clauses can be flattened by distributing their logical dependencies, enabling processing by Agenda
  • Adverse (::) and the foreigns dbsig, dberr and dberm collectively provide try./catch. functionality
  • the z-locale word assert provides a functional version of assert..
  • (Un)Fold and Z: (terminate Fold) provide break. and continue. for exiting loops early.

Note that the functional variants listed here do not attempt to emulate the "only test the first atom of the T-block" behavior that J's if. uses. For example:

if. 'chalk' = 'cheez' do. sneeze'' end.

will test true because the first atom of the evaluated T-block is 1 even if some subsequent atoms are 0:

'chalk' = 'cheez'
1 1 0 0 0

Instead, as is done in APLs and most other languages, we will assume the test verbs produce either a 1 (true) or a 0 (false) from processing the given array(s) in their entirety.

Also keep in mind that although tacit code is used in these examples, direct definitions ({{...}}) can just as well be used in their place; functional style need not be tacit.

The control-flow constructs are written below in several equivalent variations to demonstrate different coding styles.

This page assumes you are familiar with some J jargon: verbs, adverbs, conjunctions, tacit and explicit.


Flexibility of "verbs"

Wherever "apply verb f on [X and] Y" is mentioned (in this essay, and in NuVoc generally), realize that, effectively, f can always instead be:

  • any noun N (by using N"_ or equivalently [@N)
  • a verb applied on different input(s) besides [X and] Y, e.g., f@N or f&N@[ or M&f@N (equivalently, the fork M f N"_)
  • a monadic verb applied on either X or Y individually (f@[ or f@])
  • an effectful verb which may or may not process its argument(s), e.g., echo@'hello'
  • The constant verb N"_ ignores [X and] Y and gives N, and f@N is defined to be f@(N"_), i.e., it ignores [X and] Y, and gives (f N).
   99 |@_3 ]100
3
   99 (2&*@_3) 100
_6
   1  |.&0 1 2 3@[  'abc'
1 2 3 0
   1  echo@'hello' ]'abc'
hello

While loop (while.) and variations

   Wh=: ^:^:_            NB. tacit conjunction (i.e. modifier train)
   Wh=: {{u^:v^:_}}      NB. equivalent explicit definition
   2&* Wh (1000&>) 3     NB. multiply by 2 while less than 100 (monadic usage)
128
   2 * Wh (100>]) 3      NB. equivalent dyadic usage
   100 (2*]) Wh (<) 3    NB. ditto

This contruct works as follows:

  1. f^:_ (Converge) runs f on [x and] y until the result converges—in other words, until one iteration gives the same result (within the given comparison tolerance—see also Fit (Customize) as the previous iteration.
  2. as soon as the predicate fails (i.e., is false), it gives 0 as the right operand of Dynamic Power, meaning the loop body gets applied 0 times.
  3. now, since this iteration of the loop effectively did a no-op, the result is the same as that of the previous iteration.
  4. thus (^:_) sees that the loop has converged (i.e., successive iterations would all be no-ops since the predicate would fail each time from here forward), so the loop ends and the result is returned.

When called dyadically, u^:v^:_ is equivalent to (x&u)^:(x&v) y, meaning x is used as the control argument for both u and v. More commonly, though, you want x to go into the loop body but not the predicate, or vice versa. To do so, couple the control arg(s) with u and/or v. For example, to keep adding 2 while the result is less than 1000:

   1000 (2+]) Wh > 3    
   2 + Wh (1000 > ]) 3   NB. equivalent to above

NB. dyadic, keeping control as separate arg: [. Wh (].@]) [. Wh (@]) {{u Wh (v@])}}

  • Use x as control to predicate v; run loop body u monadically on y:
NB. again we can bind the control, making the entire phrase monadic:
[. Wh (x0&].)
{{u Wh (x0&v)}}

NB. dyadic, keeping control as separate arg:
[.@] Wh ].
(@]) Wh ].
{{u@] Wh v}}
  • On occasion you may want to use one control, x0, for u and another, x1, for v:
   2&+ Wh (100&>)   NB. monadic, binding the control args:

DoWhile loop (whilst.)

Run the loop body runs once unconditionally before applying the test the first time.

   dW=: Wh@:[.
   dW=: {{u Wh v @:u}}
128

Until loop

here v is the stop condition.

Til=: [. Wh (-.@:)
Til=: [. Wh (-.@:].)
Til=: {{u Wh (-.@:v)}}

DoUntil loop

v is the stop condition; run loop body u once unconditionally before applying the test v.

dU=: Til@:[.
dU=: {{u Til v @:u}}

Collect intermediate results

In any of the While constructs, use CWh=: ^:^:a: in place of Wh to instead collect the results from all iterations.


For loop (for.)

N successive applications of verb f on [X and] Y.

   [X] f&> i.N     NB. f&>@i.
   [X] f"0] i.N    NB. f"0@i.

Switch (select. case. fcase.)

   f`g`h`defVb @. (cases&i.)
   switch=: f`g`h`defVb @. (cases&i.)
   placeBuyOrder`placeSellOrder`doNothing @. ('buy';'sell';'hold'&i.) <action

Interval Switch

Applies the appropriate verb based on what interval (tier) the input fits into, e.g., different tax brackets.

   f`g`h @.(caseIntervals&I.)
   NB. e.g., f`g`h`j`defFn @. (20 55 70 90&I.)

Cond (series of if. elseif. else. statements)

Run verb f on Y if test t0 Y succeeds, else run g on Y if test t1 Y succeeds, etc., or run default verb def on Y if none of the tests succeed. The basic pattern is:

   f`g`h`def @. (1 i.~ t0,t1,t2)

This is made more readable by pairing each test with [the verb to run if the test is the first to succeed]:

   [X] (t0`f0, t1`f1, t2`f2) cond defFn Y
   cond=: {{fs`(v"_)@.(ts`:0 i. 1:)y ['ts fs'=.|:m}}
NB. cond's right operand v can be either a verb or noun; both are handled transparently

If all the "then" (as in "if…then") verbs are dyadic, their X arguments can be given as an array. If only some are dyadic, we can just bind each dyadic verb with its other argument, i.e., M&f or f&N. For example, the following:

if. (x test0 y) do. N elseif. (y test1 M) do. (x f y) elseif. (test2 x) do. (3 g 1) else. defVal end.

translates into

(test0`(N"_), test1&M@]`f, test2@[`(3&g@1)) cond defVal

or, without cond:

(N"_)`f`(3& g @1)`(defVal"_) @. ([: i.&1 test0,test1&M@],test2@[)

This version of cond works for the common case where all the tests are "pure" verbs, i.e., none of them produces side-effects, e.g. sending an email, placing a stock trade, etc. It's fine if the "then" verbs are effectful, but not if the tests are, because this version runs every test. In (rare) cases where some tests are very expensive, or where Y is enormous (say, millions of items), this could also be significantly wasteful. For such cases, use the alternate version of cond listed below.

[X] (p0`fn, p1`f1, p2`f2) {{x fs {{m @. (1 i.~ n`:0]y)}} ps y [‘ps fs’=. |:m}} Y
Short-circuiting general cond statement, using Power / DoWhile
NB. not yet tested…
[X] fs {{(m {~ >: ^:(-.@({&n`:0) *. (#n)>:]) ^:_)`:0}} ps Y
Nested if-else’s

Nested if-else's are factored with respect to their common logical dependencies, i.e., there is no redundancy in their predicates (test conditions). We can replace nested if-else’s with flat, logically redundant predicates. This makes each predicate longer (this is helped by naming them), but makes [both refactoring and reading the code] much more efficient. Refactoring is made more efficient because it gives us a single point of control (bassd on naming) for each set of dependencies on a given predicate. Contrast this with nested if-else code, where changing logical dependencies requires moving around the dependent clauses, which becomes increasingly difficult as each moves farther away from its associated predicate. In flat code, refactoring the logic for a given clause only requires editing the corresponding predicate (which is right next to it). The more if-else clauses and nesting levels you’d have in “normal” code, the bigger the difference this makes. Nesting corresponds to factoring terms; flattening is distribution of terms.

   p0=. >
   p1=. =&# +. p0
   p2=. 0=5|[ *. p1

   cond=: }. ^: {{(-.({.ts)`:0 Y +. 0=#Y ['Y ts'=.x;y}} ^:_

   cond=: {{ fs`n@. ((#@ps)`(ps i.{.)@. (*@#) y }. ^: {{(-.({.ts)`:0 Y +. 0=#Y ['Y ts'=.x;y}} ^:_ ts ['ts fs'=.|:m}}) }} defFn Y

   cond=: fs`defFn @. (Y }. ^: {{(-.({.ts)`:0 Y +. 0=#Y ['Y ts'=.x;y}} ^:_ ts ['ts fs'=.|:m }}) defFn Y

   cond=: {{pred`fn $: else ['pred fn else'=. y}}/
   cond=: {{else`fn @. (pred`:0) ['pred fn else'=. y}}/
   cond=: defFn [F.. {{else`fn @. (pred`:0) ['pred fn'=. y}}/ t0`f0, t1`f1, t2`f2

   NB. Cond using Fold (BQN style):
   NB. return the quoted function to run; defFn returned if no preds were true.
   {{[x] y {{[x] y`:0 m}} ({.v`]) [.F.. ]`({:@[ [ 1&Z:@1) @. ( {{ ({.x)`:0 }} y ) m}}
   NB. crucial: y is outside the lambda; lambda takes curr and acc, and ignores acc (it could’ve been monadic, but the former is terser.) the entire Cond has to be a lambda in order for 'y' to be defined (referenceable) here.

!!! test: does the x in [def`f@.(x`:0]Y) in a fold] even need to be passed a Y immediately there? or can we just do def`f@.(x`:0) ?? Might build up a computation of thunks like the And/Or conjunction chain. That would be great! Then we wouldn't have to worry about scoping Y within the lambda. Seems like the former is what BQN is doing.

it looks like in J this would be +`(%`-@.~:)@.= {Cond‿Act 𝕊 else: Cond◶Else‿Act}´ ⟨=‿+, ≠‿-, ÷⟩ ⟶ =◶⟨ ≠◶⟨ ÷ - ⟩ + ⟩ NB. quoted (≠◶⟨ ÷, - ⟩ is the result of first iteration within Fold.

cond=: {{(n"_ G[F.:{{y`({:x)@.({.x`:6)G}}_2]\m)`:6}} NB. translated from BQN NB. !!! note the {.x`:6 !!!

(=`+, ~:`-, $`!, ?`(+/@*"{), <&2@#@[`(*:^:(1000&>)^:a:"0)) cond 8

(".}:(=&LF ','"_`(I.@[)`]} ]){{)n = ` + ~: ` - $ ` ! ? ` (+/@*"{) <&2@#@[ ` (*:^:(1000&>)^:a:"0) }}) cond %

(".}:(=&LF ','"_`(I.@[)`]} ]){{)n (p0=. =) ` + (<echo {{5!:5<(g=.y f.)"_@[: ::]'g'}}'z') (p1=.~:) ` - (p2=.$) ` ! (p3=.?) ` (+/@*"{) (p4=.<&2@#@[) ` (*:^:(1000&>)^:a:"0) (echo {{5!:5<(g=.y f.)"_@[: ::]'g'}}&> 'p0';'p1';'p2';'p3';'p4') }}) cond %


(".}:(=&LF ','"_`(I.@[)`]} ]){{)n (p0=. =) ` + (<echo {{5!:5<(g=.y f.)"_@[: ::]'g'}}'p0') (p1=.~:) ` - (p2=.$) ` ! (p3=.?) ` (+/@*"{) (p4=.<&2@#@[) ` (*:^:(1000&>)^:a:"0) (echo {{5!:5<(g=.y f.)"_@[: ::]'g'}}&> 'p0';'p1';'p2';'p3';'p4') }}) cond %


(echo {{5!:5<'g'[g=.y}}&> 'p0';'p1';'p2';'p3';'p4')

Since we’ve flattened our predicates, a single template represents both the nested and flat/chained if-else cases.


Error handling

Assertion

   assert 1=2     NB. 0—fail
|assertion failure: assert
| assert 1=2
   assert>:i.4    NB. pass
   assert i.4     NB. fail; contains a 0
|assertion failure: assert
|       assert i.4
   assert
0 0 $ 13!:8^:((0 e. ])`(12"_))

Functional try. / catch.

the 13!:n foreigns, for (n e. 8 11 12), are [raise error], [get last error number], and [get last error message], and each has an alias for convenience.

   (dberm dbsig dberr) f.     NB. show definitions
(13!:12) (13!:8) 13!:11

We may want to catch anticipated errors while avoiding stifling unanticipated ones. If the error we catch isn't among those we anticipated, we can pass it along by signaling the original error and error message.

   f ::(handleErr6`handleErr29 catch 6 29)
   catch=: {{ m`(dberm@'' dbsig dberr@'') @. (n i. dberr'') }}
   f ::(handleErr6`handleErr29`defHandler catch 6 29)
   10 { ::(handleErr6`handleErr29 catch 6 29) i.5
NB. here e.g. handleErr6 is a verb that deals with error code 6.

To know what error codes correspond to the errors you anticipate:

   >9!:8''                          NB. list of all possible error messages
   (,&'  '@":"0@i.@# ,. >)9!:8''    NB. with indices (error codes)
0   attention interrupt       
1   break                     
2   domain error              
3   ill-formed name           
4   ill-formed number
...

Note that Adverse does not catch the special error throw. signal (35) (nor does catch. do so). throw. is only caught by catcht.:

   13!:8@34 ::1 'a'
1
   13!:8@36 ::1 'a'
1
   13!:8@35 ::1 'a'
|uncaught throw., executing monad 13!:8@35 ::1
|       13!:8@35 ::1'a'

The sole purpose of throw. is to enable us—upon encountering a problem—to set a variable that [an upstream caller using a catcht.] can then inspect, taking this or that action based on the value of said variable. In other words, throw. is just a communication mechanism between the problem-encountering function and a higher-level function; it distinguishes anticipated errors (these are the ones we throw) from system-generated errors (e.g., the length error from 0 1+2 3 4). With our functional catch, we already have more control than this, since we can take any action, at any level (in the call stack), depending on what error is raised, and/or on the contents of our custom error message, and/or a variable that was set by the error-encountering function. And we have (255 - 45 = 110) unused error codes we can raise in the "set a variable" scenario. So throw. and catcht. are obviated by the functional catch. Catch here works like a nicer catcht., and Adverse works like normal catch.


Early exit (functional break. and continue.)

Using (Un)Fold

Unfold (F. F:) (along with Z:—Terminate Fold) is the fundamental unbounded, short-circuiting loop construct. What’s more, it gives very fine-grained control over the short-circuiting logic. Dynamic Power is equally unbounded, but has no built-in break/continue mechanisms, thus requiring relatively more code to perform these.

Continue
_1 Z:1 NB. discard current iteration
0 Z:1 NB. discard post-processing (u-result) from this iteration
Break
_2 Z:1 NB. discard current iteration and break
1 Z:1 NB. finish current iteration and break
NB. Can do tacit [Unfold or short-circuiting Fold] by doing
foldIntoAcc [ _2:&Z:@1:^:pred [ _3&Z:@50
(foldIntoAcc [ Z:)^:(_2:`1:`pred)
(foldIntoAcc [ Z:)^:(_2:`pred`1:)
foldIntoAcc [ [`(_2:&Z:@1:)@.pred
foldIntoAcc [ {{_2 Z:x pred y}}
NB. explicit version, for comparison

Using Power

Continue

Continue is really just an if-statement: if this —> more processing (; else nothing).

moreProcessing ^:pred @: someProcessing @: divIntoSubProbs ^:mainPred ^:_
moreProcessing`] @. contPred @: someProcessing @: divIntoSubProbs ^: mainPred ^:_

Continuing corresponds to the implied [else nothing] branch. if [all processing within given loop] is conditional, then we need to continually update arbitrary state (which may as well be loop counter, since this is sometimes needed) in order to avoid “false convergence” and go on to next iteration. of course, you could [purposely use “continue” as a way to converge the loop by omitting the loop counter], which is an implicit way of writing “break”.

Break
NB. just add a flag to your array (either additional item w/ fill, or boxed along with all the array items), and do [flag *. pred] as your predicate. ContinueFlag if using DoWhile; BreakFlag if using DoUntil.

Break out of n loop levels

Using (Un)Fold and Z: since [a Fold broken out of by Z:] returns its accumulated result (unlike with dbsig). Mention C, Lisp, or Haskell versions for comparison.

Using Power

Well, there's really no reason to use Power for this when we have (Un)Fold—this is just an intellectual exercise to explore how we'd do this if we didn't have Fold with built-in Z:.

Just add a flag to your array (either additional item w/ fill, or boxed along with all the array items), and do [flag *. pred] as your predicate. Interpret as ContinueIterating_Flag if using While, and as BreakFlag if using Until.

So, instead of having just a break flag (0 or 1), you can have it be anything in 0 1 2 3 if you have 3 loops. 0 means don't break; 1 means break; 2 means break out 2 levels; etc. In each loop layer, the corresponding pred (if it was nested For loops--hypothetically, of course--you'd insert a predicate in each level) checks whether the breakFlagLevel is high enough that this level needs to continue the breaking-out (by failing the predicate, thus running itself 0 times, thus ending that loop level).

moreProc ` setBreakFlagLevel @.breakPred @:proc1 ^:(pred1 *. 1>breakLevel) @:proc2 ^:(pred2 *. 2>breakLevel) @:proc3 ^:(pred3 *. 3>breakLevel) ^:_   0,&<Y
NB. breakPred means the pred that determines whether we want to break.
NB. original loop, without breaks, would've been:
moreProc@:proc1 ^:pred1 @:proc2 ^:pred2 @:proc3 ^:pred3 ^:_  Y

NB. Well actually breakPred wouldn't be a pred if it outputs anything in 0 1 2 3, but whatevs.

NB. Using naming to modularize, as before:
{{
setBreakFlagLevel=. ;{:     NB. x setBreakFlagLevel y sets the breakLevel (head) in y to be x.
loop1=.moreProc ` setBreakFlagLevel @.breakPred @:proc1 ^:(pred1 *. 1>breakLevel)
loop2=. proc2 ^:(pred2 *. 2>breakLevel) 
loop3=. proc3 ^:(pred3 *. 3>breakLevel) 
loop1 @:loop2 @:loop3^:_
}}

Of course in general any or all of the loop layers might do conditional breaking.


Loop forever

Run g forever. g may be effectful, and may or may not be a function of Y; in short, it can be anything, e.g. g=: echo @('hello, ',(1!:1)@1,[@'!') [echo@'whats your name?' ((1!:1) 1 reads from stdin).

[X] [F.g Y



Computations with side effects

NB. [X] (f [ sfxVb) Y
    4 ((;+:) [ echo@'left boxed with double the right') 6
 left boxed with double the right
 ┌─┬──┐
 │4│12│
 └─┴──┘
    sFX=: [.[ ].
    4 (;+:) sFX (echo@'left boxed with double the right') 6
 left boxed with double the right
 ┌─┬──┐
 │4│12│
 └─┴──┘
 
For more on isolating side-effectful verbs see Pascal Jasmin's page on the subject.

Also be sure to mention when it's not appropriate to use these. Usually, instead of using continue., we just filter out the elements we would've skipped. Instead of using break., we usually just apply the break predicate to the array, take up to the index of the first element that passed the test ({.@(pred i. 1:)), and apply our verb on that. Instead of wrapping a IndexOf phrase in a try./catch. expression, catching a potential index error, and specifying a value to return, we just append the default value to the end of x and rely on i.'s "#x on not-found" behavior (([,defVal"_){~i.) C.f. J Wiki's Loopless page for tons of much better examples, and also link to it.

NB. verb to modify a variable
z=:'abc'
{{0 0$z=:|.z}} 5

Make all real examples so the reader can interact with and tweak them.