Plain GROUP! & Branching: Only Run If Branch Taken?

Something that came up early on was the question of parity in how CASE dealt with GROUP! branches and how IF dealt with GROUP! branches.

Because IF evaluated its arguments, the GROUP! would always run...even if the condition ruled out the BLOCK! that was evaluated to running:

 rebol2>> if true (print "in group" [print "in block"])
 in group
 in block

 rebol2>> if false (print "in group" [print "in block"])
 in group  ; ...still printed...just no "in block"

But with CASE, the branches lived inside a block passed in...making them effectively quoted. Which meant not only could they be skipped, but they were skipped:

rebol2>> case [true (print "in group" [print "in block"])]
in group
in block

rebol2>> case [false (print "in group" [print "in block"])]
== none  ; didn't say "in group" this time

Sensing something was amiss, Ren-C brought these two situations into sync... but biased to the IF behavior. When a CASE saw a GROUP! in a branch slot it would run it, just as an IF would...regardless of whether that branch would run.

early-ren-c>> case [false (print "in group" [print "in block"])]
in group
; null

Choosing it this way wasn't due to thinking it was the more useful behavior, but because it was perceived as the only option.

There seemed no way to stop the GROUP!s from running in IF. So long as the branches were evaluative, this was the only way to get parity...

Should Plain GROUP!s Only Run If The Branch Is Taken?

What if the branching constructs took the GROUP!s literally, and ran them to generate branches only if the condition matched?

>> branchy: func [flag] [either flag '[<a>] '[<b>]]

>> either okay (print "a" branchy okay) (print "b" branchy null)
a
== <a>

>> case [1 = 2 (print "in group" [print "in block"])]
== \~null~\  ; antiform

I imagine if you conducted a poll, you'd find the majority who'd say they'd prefer that behavior.

A handful people might say it rocks the boat to have a GROUP! at a callsite that doesn't execute unconditionally. But branch slots are already rocking the boat...they're not evaluative, so they can see quotes:

>> if okay '<foo>
== <foo>

(You can read all about it with Literal Branching...if you've forgotten)

What Would This Inhibit?

For most cases, not running the group unless the branch runs is better. Even if your branch-making code has no side-effects, it's better for performance not to run it unless you have to. Lambdas are a good example...if you wanted to say switch x [...] then (x -> [...]) you could avoid an entire function generation just by having it in a GROUP! like this.

But if you imagine something like a "branch generator" that generates branches in a certain order, and you wanted to position them using that order...you'd need to use a COMPOSE:

eval compose [either (branch-gen) (branch-gen)]

I don't think that's too much of a tax to pay for that intent. It looks fine and conveys exactly what it is...clearly running both branch generations for the COMPOSE step prior to the EITHER.

What I'm more hesitant about is that literal behavior for branches to not run GROUP!s at the callsites is a fairly early curveball.

But maybe it's just an example of the language being fluid.

Drat! A Mechanical Foil

(I'm not sure if the practice of writing out the reasoning before trying experiments is a good one, because I usually find some issue pretty quickly when I try them...but...)

So the feature of Lambdas for Branches relies on something that is tied to non-hard-literal semantics.

Rightly or wrongly... the decision was that in an "escapable" literal slot (e.g. one that would accept a GROUP! and evaluate code)...that escapable slot was also willing to accept something produced by a left-literal construct on the right hand side.

I'm not too keen on coming up with another parameter convention that is "hard literal unless a lambda decides it wants to evaluate and put something there".

But there are still avenues of attack...

One might be that GET-GROUP! is the "sneaky literal defeater" instead of plain GROUP!. When you think about it, if X and X/Y are literal for soft literals but :X and :X/Y aren't... why wouldn't it be :(FOO BAR) instead of (FOO BAR) to undermine a literal site...with ordinary GROUP!s literalized normally?

So like this:

>> either true (print "a" [<a>]) (print "b" [<b>])
a
== <a>

>> either true :(print "a" [<a>]) :(print "b" [<b>])
a
b
== <a>

To me, that feels more natural...that the colon is asking for subversion of the quote.

Interestingly, you could mix and match...here, seen getting it out of order:

>> either true (print "a" [<a>]) :(print "b" [<b>])
b
a
== <a>

If it were done this way, then it suggests that GROUP! is acceptable as a parameter for a literal argument...but what if you got a GROUP! produced by the subverted quoting?

>> if true :(print "a" $(print "b" [<c>]))
a
b
== <c>

That's just what would naturally happen. Seems all right to me, but it's not clear how you would stop it if you wanted to (as in today's situation, you wouldn't know as the recipient of an escaped literal if what you got was generated by a literal or not.) So if this bothered you, you couldn't use escaping and would have to hard literal it.

This idea feels rather appealing, but it suggests a different semiotics for arguments. e.g. an escapable quoted parameter would be :x or ':x... not '(x)

Anyway, what this really does is raise some questions about how people feel about literal groups and branching.

I am finding myself leaning pretty heavily on the side of saying that it is most convenient if a plain GROUP! in a branch slot does not run its branch generation behavior unless the branch is taken. Which is a bit of a shift for me...as I'd favored "normal" evaluation semantics more often. But it's just seeming that practically speaking it's cleaner to leverage the literals.

While no one has really commented on this, I think my mind is made up:

It's more intuitive and useful to NOT run the plain GROUP! when the triggering condition is false.

Mechanically, Ren-C has long been able to make a different choice from historical Redbol here, due to changing the parameter conventions to facilitate literal branching.

...speaking of which, my mind is made up on that, too:

Literal branching is now foundational. I use it all the time, and like it. And it's an even bigger efficiency advantage in stackless, where you can provide a value directly and not require pushing a stack frame.

So the decision is made.

What if You Want the Branch to Run Anyway?

Right now the answer is to use a GET-GROUP!.

>> branchy: func [flag] [either flag '[<a>] '[<b>]]

>> either okay (print "a" branchy okay) (print "b" branchy null)
a
== <a>

>> either true :(print "a" branchy okay) :(print "b" branchy null)
a
b
== <a>

The reason it works is that right now, GET-GROUP! unconditional processing is done by the soft-literal parameter convention. So things like EITHER don't have a choice. They receive a plain GROUP! literally, but a GET-GROUP! will have ran before their code starts.

But I doubt anyone was really clamoring for the ability to run code in groups for branches that aren't taken. If you are deeply affected, feel free to write a long sad essay here. :sob:

1 Like

Leading colons won't mean "GET-" of anything. It will be used for refinements.

That doesn't necessarily mean it can't be used for escaping. But...

Note that R3-Alpha and Red used plain GROUP! for escaping "soft literal" arguments... but then also permitted GET-WORD! and GET-PATH! so you could avoid using a group. Allowing you to say for-each :var instead of for-each (var) would mean you weren't creating a group! that might interfere with COMPOSE-ing (and I'm sure they also considered the "efficiency" of not allocating an array).

I changed it to GET-GROUP! for escaping, making plain GROUP! passed and typechecked literally. My hunch was that the only reason that they didn't use a colon-prefixed group for the special "escaped" state was because they didn't have it for groups.

But really... this escaping is pretty rare (and will be even rarer now that loop variables aren't taken literally!). And I've honestly always thought the colons looked ugly.

What would you rather read?

 >> integer! = (first [type length]) of 1
 == ~okay~  ; anti

 >> integer! = :(first [type length]) of 1
 == ~okay~  ; anti

I think it has just sort of emerged that functions which look for their arguments escapably-literally are not looking for literal GROUP!s. It hasn't come up. You either care about groups literally and don't escape, or you're escaping and don't have meaning for groups.

If you find yourself backed into a corner with a function that is forcing you to pass a literal argument when you don't want to do that... APPLY the function to bypass callsite conventions. Or skin it with a new parameter convention.

So...I think I'm just going to axe the whole "colon escaping" idea. GROUP! or bust.

I'll mention a potentially mindbending idea:

What if there were no such thing as escapable-literal arguments... only literal arguments. But if they don't typecheck GROUP!, then the GROUP! evaluates.

e.g. any literal parameter which doesn't want GROUP! can be escaped via group. It could be an automatic feature of any literalizing construct for which groups aren't meaningful.

This helps with the "what happens when an escapable parameter takes a group, but evaluates to a group" question.

:thinking:

It certainly wouldn't make me upset to drop a parameter convention. But it does sound a bit wacky.

This remains a very irritating thorn.

But I am 100% convinced that branches need to take GROUP!s literally (and not run the branch generation code unless the branch is taken.)

So we have to now ask how convinced I am that you need to be able to write:

 except e -> [...]

...instead of being forced to write:

except (e -> [...])

Kill Your Darlings (?) :slightly_frowning_face:

Historically I've been proud of how the avoidance of the GROUP! is accomplished, as it was technically quite tricky...

...but this "cleverness" has costs. (Not the least of which is explaining that the feature exists at all, and how it works!)

Plus I've mentioned that if you don't have the GROUP! there, the arrow has to run unconditionally...AND it has to produce some kind of ACTION! regardless of if it's going to be used or not.

I'll mention the need for literalism has pushed groups into more places. e.g. things like FUNC have to take their body literally now, or else it can't inspect a FENCE!'s top-level set-words to fold them into the frame.

Hence we can't write:

foo: func [...] compose [...]

But have to say:

foo: func [...] (compose [...])

(Although there's a backdoor with foo: func block! [[...] [...]] if you need it, in code composition scenarios where you don't want groups.)

So if you find you have to write:

except (e -> [
    ...
    ...
    ...
])

Instead of:

except e -> [
    ...
    ...
    ...
]

How bad is it the group really? It's more cognitively honest. It's better for performance.

This Is A Tough Call

I will definitely say that accepting the GROUP! for generative code in function bodies has reset my thinking.

And when I think about the scenarios of some of the most "showcase" code (like UPARSE) I would have to use a GROUP! anyway, because of the performance impact from not using one.

If I can't use my "clever" feature in showcase code, where should I use it?

Not to mention the fact that TRAP has gotten rid of the pattern (e -> [return fail e]) which was the most common place where this mattered.

Future Versions Could Override My Call

Let's say I'm wrong. You would have always been able to use a group.

Maybe this is the best argument of all. Take it out, stop worrying about it, let the future worry about this one. :robot:

1 Like

Maybe in the end it’s slick but YAGNI.

1 Like

Looking at examples in practice, I don't know I'm as convinced it's "good" as I thought:

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then x -> [
    assert [x = <yes>]
    1000 + 20
]
assert [y = 10]

vs.

y: case [
    1 > 2 [<no>]
    1 < 2 [<yes>]
] then (x -> [
    assert [x = <yes>]
    1000 + 20
])
assert [y = 10]

When you're used to thinking of branches taking their arguments literally, it's "spooky" to reason about it. What's happening, what order does this run in, etc.

It's hard enough to grasp that a GROUP! is literal and not evaluated unless the branch is taken... but that's reasonable. e.g. people would intuitively say:

"y'know it's probably better if the code for creating the branch doesn't evaluate unless it's going to be taken, so I hope that's what it does."

But what would the intuition say about running that ARROW function? There's no real intuition at work, you have to learn weird rules, reaction would probably be:

"oh. huh. it resolved a literalism fight by letting the right hand side win, because of... some flag somewhere? and it generates a function even if the branch isn't taken? but you optimized that weirdly so it's not as bad as it would be? :face_with_diagonal_mouth: weird."

Think I'm decided now. You need the group.

1 Like