Why Do No-Op Loops Evaluate GROUP! Bodies?

This does sound tempting. But we have to confront something that's fundamentally different about branches and loop bodies:

  • Branches are meant to run zero or one times

  • Loops are meant to run zero to infinity times

If loops are to squirrel away their branches as literal, and receive a GROUP!, then they're in the awkward position of having to make their first run different.

This might sound like just a little tweak to the body-running logic, to say "if you get to the point of running the body, if it's a group, then EVAL it and re-store it in the body variable...so you don't run it again".

But the implications are deeper than that. It means the decision to do this transformation happens inside the loop, and that doesn't compose well.

Consider the implications of literal bodies on something like the FOR-BOTH loop wrapper...

Today's FOR-BOTH doesn't take the body literally:

for-both: lambda [@(var) blk1 blk2 body] [
      all [
          ^ for-each (var) blk1 body then lift/
          ^ for-each (var) blk2 body then lift/
      ] then unlift/
  ]

If it did take the body fully literally, you'd have to use trickery (COMPOSE or APPLY) to subvert it for composition, so it would look like:

for-both: lambda [@(var) blk1 blk2 @body] [
      all [
          ^ for-each // [var blk1 body] then lift/
          ^ for-each // [var blk2 body] then lift/
      ] then unlift/
  ]

But if you pass a GROUP!, now each FOR-EACH gets a GROUP!...and each makes the decision to transform it, resulting in two body evaluations.

>> for-both item [1 2] [3 4] (print ":-(" reverse copy [item print])
:-(
1
2
:-(
3
4

You're now worse-off than you were with evaluating the branch. Not only is the performance worse, but you don't even know if that code branch-generating code was semantically safe to run twice!

The only thing FOR-BOTH could really do in such a situation would be to EVAL groups ahead of time:

for-both: lambda [@(var) blk1 blk2 @(body)] [
      if group? body [body: eval body]
      all [
          ^ for-each // [var blk1 body] then lift/
          ^ for-each // [var blk2 body] then lift/
      ] then unlift/
  ]

But if we have to do that... we're doing the same thing as taking the branch evaluatively, except we've made our code uglier.

Workaround: Don't Use GROUP!, Use CACHE

If there's some costly transformation you think you can avoid in the "not ever actually called" case, don't use a GROUP!. Instead, make your code an ordinary BLOCK! but cache the calculation:

 for-each item data-maybe-empty [
     eval cache [some-expensive-transformation [
         ...
     ]]
 ]

Also: opting out of the body is a good way to dodge wrapping work when you don't want to run the loop, as opposed to opting out of the data:

for-each 'x data (if condition [[
    x: ... ; ordinary block produced by IF branch, no wrapping
    ...
]])

for-each 'x data (if condition [{
    x: ... ; wrapped block produced by FENCE!, new var
    ...
}])

You can write that more succinctly without the parentheses and using $[...] in the non-wrapped case:

>> if 1 = 1 $[stuff]
== [stuff]  ; bound

for-each 'x data if condition $[
    ...
]

But pursuant to the argument above, this trick can't be used with fences:

>> if 1 = 1 $[stuff]
== {stuff}  ; bound

for-each 'x data if condition ${  ; FOR-EACH doesn't take FENCE!
    ...
}

If FOR-EACH were tolerant of accepting FENCE! by value you get into that problem of the loop wrappers having something which has to have a "moment" of wrapping digestion. And in practice that moment must happen unconditionally at entry to the function.

Since the caller can't expect any different semantics from wrapping before they called, we aren't doing anyone any favors by forcing that work onto every loop wrapper. It hides the mechanic and obscures the reality.

Hence opting out of a body that you want fenced is at minimum:

for-each 'x data if condition [{
    ...
}]