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.

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

It wasn't really a question of which was the more -useful- behavior. Choosing it this way was because it was perceived as the only option. There was 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...

Suppressing Evaluation Considered for @(gr o up)

When trying to figure out what it was that @(...) did, one potential feature was to remedy this unwanted execution. It would be like (...) but only run if the branch executed.

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

 >> either true (print "a" branchy true) (print "b" branchy false)
 a
 b
 == <a>

>> either true @(print "a" branchy true) @(print "b" branchy false)
a
== <a>

Useful. But then @(...) became desired for other purposes.

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

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

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

 >> either true (print "a" branchy true) (print "b" branchy false)
 a
 == <a>

>> case [false (print "in group" [print "in block"])]
; null

I imagine if you conducted a poll, you'd find a fair number of people who'd say they'd prefer that behavior...and would prefer this to using @(...) to get it.

Other 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 true '<foo>
 == <foo>

(You can read all about it with Soft-Quoted 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:

 do 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 hard-quoting 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-quoting semantics.

Rightly or wrongly... the decision was that in an "escapable" quoting 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-quoting construct on the right hand side.

I'm not too keen on coming up with another parameter convention that is "hard quoted unless a lambda decides it wants to evaluate and put something there". So that sort of kills this idea...so long as it's plain GROUP! that is considered to be what defeats quoting.

But there are still avenues of attack...

One might be that GET-GROUP! is the "sneaky quoting defeater" instead of plain GROUP!. When you think about it, if X and X/Y are quoted by soft quotes but :X and :X/Y aren't... why wouldn't it be :(FOO BAR) instead of (FOO BAR) to undermine a quoting site...with ordinary GROUP!s quoted 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 from direct quoting...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 quote if what you got was generated by an escape or not.) So if this bothered you, you couldn't use escaping and would have to hard quote 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 the quoting of 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 quoting.

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 soft-quoted branching.

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

Soft-quoted 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 true (print "a" branchy true) (print "b" branchy false)
a
== <a>

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

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

I'm less sure about this, and it ties into a lot of questions about parameter conventions and things-that-start-with-colon. Some of the more interesting ideas I have for GET-XXX have to do with a meta-like convention that would allow the implementations of things like APPEND to be able to reduce-and-splice:

Beating REPEND: A New Parameter Convention?

So I'm unsure of how what might come out of that could affect these choices. If APPEND expects to receive things like a GET-BLOCK! unadulterated so it can blend reducing with appending, they might expect GET-GROUP!s to be passed as-is also. That might juggle things in these parameter convention ideas.

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.

THE-GROUP! and biasing the other way is still on the table

I mean... I guess we could do that?

I Hate That I Still Have To Think About This. :face_with_head_bandage:

My intuition is still that not running groups unless the branch is going to be taken is the more useful behavior. But... when you look at the available parts, the cleanest way to provide both capabilities is to say that the plain GROUP! evaluates unconditionally... and the THE-GROUP! only runs if the branch runs.

I don't personally generate branches all that often, and if I do it's in COMPOSE'd code... so the compose happens all at once without worrying too much about not running the branch generation in the case the branch isn't taken.

But it is an interesting nuance, and I might be more likely to take advantage of it if I didn't know this behavior has been in such flux.

As it turns out, there's no real choice but to run the groups unconditionally now.

Changing escapable literals to use plain groups means that so long as branching structures use escapable literals for the branch, then groups will execute unconditionally and typecheck the result.

Changing away from escapable literals to use non-escapable ones would break the ability to get implicit tiebreaking and you couldn't use lambdas on the right of things like THEN and ELSE without putting them in groups.

Going away from literal arguments for branches altogether would kill off soft-quoting branching

So if there's going to be a form of branch that doesn't calculate a synthesized branch unless the branch is going to be taken... I think THE-GROUP! is the answer.

So there's been a little bit of a twist, in that I'm leaning toward saying that obj.@x is equivalent to obj.(x).

(Note that we couldn't really use obj.:x for this anymore even if we wanted to. Because that's not a TUPLE! which holds [obj :x], it's a CHAIN! that holds [obj. x], which is structurally wrong for the purpose.)

So @ is moving in on the space of being kind of like "indirect the thing, go fetch its value". That resembles more its purpose in parse:

>> var: [some "a"]

>> parse [[some "a"] [some "a"]] [some @var]
== [some "a"]

And when you say fail @var then FAIL has both the name of the variable, and the idea that you want to fetch that value as problematic. So if var is 2 it can say: "Bad Value for VAR: 2"

So Could @ Actually Be The Subversive Escaper?

This is the reverse of my original suggestion:

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

>> either okay @(print "a" branchy true) @(print "b" branchy false)
a
b
== <a>

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

It seems counter-intuitive if what you have in your head is that @ is a "barrier to evaluation". Because it seems like you'd plunk that heavy thing on a group to protect it from being evaluated:

>> (1 + 2)
== 3

>> @(1 + 2)
== @(1 + 2)

But at this point, defining @ through the lens of "barrier to evaluation" just because EVAL doesn't do anything with it (besides bind it) seems limited. That's one role it plays. In other contexts, it serves as a signal with varying meanings.

And here we have a pretty clear case where we know the common desire of the undecorated group is not to run the code for the branch unless the branch runs. Any work you do on a branch e.g. to WRAP it or otherwise bind it for execution is not work you want to do. So is decorating it to make the request to evaluate escapedly so strange?

This is based on the concept that things like @a or @a.b would also be means of escaping the quote.

 >> foo: func [@(var) [word!]] [print ["Var is" (mold var)]]

 >> foo x
 Var is x

 >> word: 'hello

 >> foo @word
 Var is hello

 >> foo @(first [a b])
 Var is a

 >> foo (first [a b])
 ** Error: foo doesn't take GROUP! as its var argument

If we instead were to say that plain GROUP!s were what we escape, that plainness means it wouldn't be part of a family of things that escape.

Or :man_facepalming: Should There Be More Nuance In The Model...

I'm really trying to avoid the idea of having multiple different soft-literal escaping conventions.

The idea that some callsites work better with plain GROUP!s for escaping their literal slot vs. others working better with @-GROUP! and have to specify that on their interface is a complexity tax I'd like to avoid.

Now that FOR-EACH doesn't take its argument literally anymore, the big places we're looking at literal escaping in the wild are branches and the AND and OR statements.

if x and (all [y z]) [...]

In those cases they want the GROUP! literally by default, so they can avoid running them.

Another place would be OF:

(first [type length]) of series

In that case, it's inconvenient for OF to have to EVAL the group and typecheck the result.

But anyplace where what you take literally is code, then it's a more powerful default to not run it.

WAITAMINUTE. Should This Be Driven By Typechecking?

It seems to me that the places that you don't want the groups to evaluate are the places for which GROUP! is a valid argument type.

So if you see a group at the callsite, typecheck and it says "yup I'll take a group", pass it the group.

Otherwise, evaluate the group and pass it the product.

There may be something to that. It might make it a little harder to decipher at the callsite as to what sort of behavior you're going to get just from reading the spec, but I'm trying to do anything I can to avoid adding another parameter convention.