The Simple-yet-Powerful Magic of The Loop Result Protocol


#1

UPDATE 8-Jun-2018: This post has been updated to reflect new rules.
UPDATE 22-Jun-2018: Updated again to reflect use of void

I showed in my previous post how to react to the result of a loop, based on the knowledge that it returns (VOID!) if it never runs. This lets you transform:

if condition [
    while [condition] [
         stuff to do in the loop
    ]
] else [
    stuff to do if condition was false to start with
]

Into:

while [condition] [
    stuff to do in the loop
    true
] or [
    stuff to do if condition was false to start with
]

That’s pretty neat. And it’s not just about avoiding repetition for the sake of brevity… your condition might want to have side effects, so having a way of phrasing it that you don’t double-execute it could be semantic as well.

But there’s another signal to know about, and that’s that a loop which BREAKs returns null. And if the body of a loop returns null, that will be turned into a VOID!:

 >> for-each item [1 2 3] []
 == #[void]

But you’re not limited to this. As long as you’re willing to have your body return some non-null/non-void thing, you have three signals:

  • null -> loop was broken, didn’t run to completion
  • void -> loop body never ran, not even once
  • non-void thing -> loop made it to the end

…and it’s Awesome

There are lots of patterns at your disposal.

Remember that DID and NOT now treat nulls as false. So you can squash together the null and blank states.

 opt some-loop [
     ...
     true
 ] else [
     code to run if loop had a BREAK, or body never ran at all
 ]

 opt some-loop [
     ...
     true
 ] then [
     code to run if loop ran at least once, and didn't break
 ]

 some-loop [
     ...
 ] else [
     code to run if loop BREAKs
 ]

The sky’s the limit with these things:

opt some-loop [
    ...
    true
] then [
    code to run if loop ran at least once
] else [
    code to run if loop never ran, or break
]

Practical example?

Here’s a real world case from the console code:

pos: molded: mold/limit :v 2048
loop 20 [
    pos: (next find pos newline) else [break]
] also [
    insert clear pos "..."
]

You have up to 2048 characters of data coming back from the mold, ok. Now you want just the first 20 lines of that. If truncation is necessary, put an ellipsis on the end.

loop 20 obviously will always try and run the body at least once. (So the loop will never return void here.)

FIND will return blank if it can’t find the thing you asked it, then NEXT of a blank is null. So, the ELSE runs when you try to get the NEXT of a blank, but cannot. If it makes it up to 20 without breaking, then the ALSO clause runs.

So there you go. The first 20 lines of the first 2048 characters of a mold, truncating with “…” I think the ALSO really follows the idea of completion, it makes sense (to me) that a BREAK would bypass an ALSO clause.

I encourage people to get creative, to look at ways to use this to write clearer/shorter/better code.


#2

I really like it. I’m curious to see what others have to say about it. I’m not the most clever ren-c coder, but I definitely see some of these new constructs enabling me to sidestep some of the bulkier expressions I often write.


#4

I’ve updated the post to convey a new–and I believe less “meddling”–version of the rules.

The previous idea of formalizing that a loop which never ran would return NULL arose from historical Rebol–as well as a desire to avoid fabricating a value when there was none. So when it came time to think of how a loop would signal to the outside that it had been broken, NULL was “already taken”. That’s how the idea of blank when a BREAK happened came up.

But I thought to look at it from a fresh frame of mind, after the recent reflection on why having failed conditionals return BLANK! is a bad idea. BLANK! is a legitimate value, and doing mutations of it to push such an in-band value to become some other in-band value…just to be able to use BLANK! to signal something else…is bad.

NULL is different. It can’t be stored in a block, it’s neither true nor false, and historical Rebol wouldn’t even let you use it to unset a variable without a special refinement to SET. It is supposed to be “edgy”. So should a conditional branch evaluate to a null, or a loop body evaluate to a null, it’s not such a terrible sin to convert it to a BLANK!. But it’s much worse to convert another value, especially to convert a LOGIC! false into a (truthy) BAR!.

So this new spin on loop rules reassigns NULL to loops getting a BREAK. A loop that never runs gives a BLANK!, which is also a legal value for if the loop body wants to return that. but a loop user who wants to be complicit in distinguishing a loop that never ran from one that did may do so, simply by saying:

 while [...] [
     ...
     true
 ]

That gives null if it breaks, true if it runs at least once, and blank if it never runs. So you get the three states, albeit having to get a little more involved. (Code golfers will generally find whatever the body returned naturally was already truthy, but I think explicitly putting the TRUE is wiser for the common codebase).

I think this is on the whole a better plan–giving basically all the same benefits, but in a clearer way.


#5

Sounds good. You’ve thought it through very carefully, now we just need to try it on for size and see if it’s a good fit.


#6

I must admit I felt a bit uneasy about barification, so I like the new turn.


#7

I must admit I felt a bit uneasy about barification, so I like the new turn.

Me too…and hopefully you will also like the end of blankification (now a more limited form of result-mutating, known as voidification).

…which makes me feel less uneasy, also–especially in being able to cut out all those sprawling refinements and *-specializations…