Brevity in the Box: When Is It Worth It?

(cc: @razetime, please let me know if any of this makes sense, or if not what parts don't)

Now that I'm again revisiting Rebmu, the familiar pattern of "hey wait, should this be a default language feature?" comes up again.

A Simple Example Of The Pattern

Just imagine a shortcut for something like WHILE, where if you give it a WORD! as the condition, it acts as if you put the condition in a block:

 x: true
 while 'x [...]

That's shorter than while [x] [...]. But to get it even shorter, while could quote its first argument.

 x: true
 while x [...]  ; doesn't eval X at callsite, so WHILE gets the WORD! itself

The downside to this is that if the condition is generated by code, you'd have to escape the quoting somehow. Otherwise while reduce [some condition] [some body] would be interpreted expecting REDUCE to be an arity-0 function called each time as the condition check. Then, the [some condition] block would wind up in the body slot. So [some body] would get inertly discarded as the next expression.

This Is Harder To Teach

A new user has a tough time getting their head around the simple question of why IF can't have a BLOCK! around its condition, but WHILE requires one. It takes a while to instill this "obvious" idea and convince people that it is a design feature vs. a bug. (@razetime - it would be a good idea to be able to write out--in your own words--why Rebol's IF doesn't take a BLOCK! as its condition, but WHILE needs it.)

Though changing the convention would have its share of surprises, this isn't to say that the current behavior doesn't have its own confusing possibilities. With evaluative conditions, you could leave the block off on accident and if it incidentally was a block, you could run something as code:

 data: [[print "formatting hard drive"] [print "sorting MP3 collection"]]

 x: first data
 while x [...]   ; imagine they meant to say `while [x] [...]`

You can still make mistakes under today's semantics, as the code above shows. It can be argued that the mistake is more consistent, as the argument is simply being evaluated like that of APPEND or other primitives. Though that consistency doesn't magically mean it's any less confusing.

Might Quoting-but-Erroring Provide a Smooth Continuum?

Continuing to consider this example, we might imagine that it quotes the condition argument in the default implementation but refuses to run anything but BLOCK! and GROUP! This would pave the way for compatibility with code that gave meaning to the quoted case.

 import <core>  ; imagine this is how you get the default definitions

 x: true
 while x [...]  ; !!! Error, WORD! not accepted as argument
 while 'x [...]  ; maybe this would be okay vs. `while [x]`, though?

Then if you did some kind of expert mode import, it would become more lax

 import <core>/lax   ; let's say you can provide switches to IMPORT

 x: true
 while x [...]  ; let's say lax meant this acts as `while [x]`

This might be reasonable. And it may provide an answer to the issues I bring up in "Speaking with Tics". Maybe a strict mode makes you say for-each 'x [...] and 'type of x and then there's a wholesale switch on arguments that lets you dodge quoting.

Another Example: DOES

Today's DOES is actually taken from Rebmu, because it lets you avoid putting the body of the DOES in an outermost block:

 >> old-way: does [print "Hello"]
 >> old-way
 Hello

 >> new-way: does print "Hello"
 >> new-way
 Hello

I thought this was cool enough to adopt. But this has that characteristic pattern of needing escaping when the body is generated from code:

 rebol2>> old-way: does reverse ["Different" print]  ; reversal is body
 rebol2>> old-way
 Different  ; printed

 >> new-way: does reverse ["Different" print]  ; specializes REVERSE
 >> new-way
 == [print "Different"]  ; block value, reversed each time
 >> new-way
 == ["Different" print]  ; it's the same block, so doubly-reversed now

 >> new-compatible: does :(reverse ["Different" print])  ; reversal as body
 >> new-compatible
 Different  ; printed

Note that the DOES handling for GROUP! isn't escaped so you have to use :(reverse ["Different" print]). Whether we think this needs fixing or not depends on if we decide that does (...) has an interesting unique meaning, e.g. does (elide print "vanishes") would be invisible while does [elide print "vanishes"] would be void. In other words, does (x) is currently acting as do '(x) would, while does :(x) is acting like do x. I'm not 100% sure either way right now.

This feels like something positive to have in the box as a default, as opposed to just being a fringe Rebmu-ism. Especially because DOES lacks a RETURN statement, isn't it nice to be able to say:

 helper: does catch [
      if condition [throw 10]
      throw 20
 ]

 ; Compare with...

 helper: does [catch [
      if condition [throw 10]
      throw 20
 ]]

But notice the implication here gets to where DOES needs to be variadic. The mechanic of doing something along the lines of POINTFREE for a parameter is something that's a pain to have to rewrite every time, and it means the argument is harder to fill with in FRAME!s with specializations. That suggests it should probably be a parameter convention in its own right. :-/

How Far Should This Go?

Much like the points I raise in "Speaking With Tics" regarding shorthand, it's hard to say.

DOES is a pretty good poster child for the question. A reasonable hedging strategy might be to reserve the right to make DOES be "clever" in the future by quoting its argument, but disallowing WORD!s for now. So if you produce the thing-to-do with code, you have to put that code in a GET-GROUP! as does :(...).

But I have mixed feelings about extending this approach places. If you want to do some pre-binding on a function body, do you want to have to write:

 func [...] :(in some-context [
     ...
 ])

Or is it too "Rebol-like" to have these meta-coding experiments not need parentheses:

 func [...] in some-context [
     ...
 ]

Key to the question under debate is if it would be more common for people to want to write:

 func [...] case [
     ...
 ]

...insead of:

 func [...] [
     case [
         ...
     ]
 ]

When Rebmu is considered, meta-coding is the rarer need, so that seems to favor optimizing out the brackets in the non-meta cases and paying for the group in the meta case. But clearly the mechanics get weird. As an example, think about:

 func [x] if x [
     ...
 ]

For that to be equivalent to func [x] [if x [...]], the variadic expression needs to bind into the spec. So that X can't be specialized as whatever it was in the enclosing context. So a magic parameter convention which was willing to specialize the body as an ACTION! couldn't be used, as it would need to be informed by the binding logic of FUNC itself. This points to an advantage of getting things in blocks.

Lowest Common Denominator In Box, Then Let Users Decide?

Imagine we say that a function body can be only two things, a BLOCK! or a GET-GROUP!.

That could be the standard that you encourage to work for any variant of FUNC/FUNCTION. But beyond that, each module could pick its conventions...or even change conventions on an impromptu basis (per class, per function, or per scope-in-function even).

One convention might say:

func [x] reverse [...]  =>  func [x] :(reverse [...])   ; reversed block is body

Another convention might say:

func [x] reverse [y]  =>  func [x] [reverse [...]]   ; reverse upon invocation

Restricting to [...] and :(...) by default doesn't seem too terribly antagonistic. The policy needs a name, something like "the baseline block rule". Then focus on facilities for easily customizing local definitions of things like FUNC or WHILE to be more creative, instead of trying to prescribe the shape of that creativity in the natives themselves.

One advantage of a baseline block proposal is better learnability for new users. Recall the example I gave up top about WHILE working "incidentally" because the value you forgot to put in a block turned out to just happen to evaluate to a block:

 data: [[print "formatting hard drive"] [print "sorting MP3 collection"]]

 x: first data
 while x [...]   ; imagine they meant to say `while [x] [...]`

But with baseline block, that'd be an error. They'd have to clarify it as either while [x] or while :(x).

Then we focus on figuring out how to make it easier and easier for people to bend this when they get new ideas. Maybe while 7 => (x q) [body] signals something of interest to a budding language designer, and they want to build a detection pattern for INTEGER! => GROUP! that applies in while conditions only... but they'd like all their normal whiles to keep working. :man_shrugging:

This way, the golfing adaptations become just a sample of the kinds of adaptations you might choose. And if you liked the Rebmu choices at a conceptual level, you could import those without having to also adopt all the short names as well.

2 Likes

In the beginning it was hard to grasp why WHILE had a block instead of just a condition or conditions, but it is a feature, because the block! will be evaluated which makes more complex things possible.
Having the WORD! as an alternative would be confusing, but only at first as are many things Rebol. (And that is NOT a BADTHING!)

The DOES native no longer... does this. Especially because its meaning is in flux... DOES is being limited to BLOCK! for now.

But here's a bit of code from the tests for DOES+ which has this behavior.

(does+: reframer lambda [f [frame!]] [
    does [eval copy f]
]
ok)

(
    backup: block: copy [a b]
    f: does+ append block [c d]
    f
    block: copy [x y]
    f
    all [
        backup = [a b [c d] [c d]]
        block = [x y]
    ]
)

(
    x: 10
    y: 20
    flag: 'true
    z: does+ all [x: x + 1, true? flag, y: y + 2, <finish>]
    all [
        z = <finish>, x = 11, y = 22
        elide (flag: 'false)
        z = null, x = 12, y = 22
    ]
)

(
    catcher: does+ catch [throw 10]
    catcher = 10
)

But what do people think? Is that cool, or bad? It captures all the variables at the state they are at the time of specialization, so it's different from putting code in a block. That could be rethought, but also it does offer a bit of optimization if it's what you want...and is different from giving a BLOCK!.

I note that this doesn't work with GROUP!s, but it should:

>> code: [print "Hello"]
== [print "Hello"]

>> x: does+ (code)
** Error: Actions made with REFRAMER cannot work with GROUP!s

There isn't any real reason that shouldn't work. It could just implicitly say the action it is reframing is EVAL if it's a GROUP!. Well, I'll get to that someday. :yawning_face: