Evaluating the worthiness of VALUE_FLAG_UNEVALUATED


#1

One question that comes up fairly often with new users is why if [var = value] […] doesn’t do what they expect.

We know that blocks are truthy, and passing a literal block to an IF will not run the code in the block. They should have written if (var = value) […] or just plain if var = value […]. But this isn’t obvious to a newbie.

When I’ve encountered this situation myself, it’s usually because of changing a WHILE into an IF, or something of that sort. Of course, I know why a WHILE puts a block around its condition and an IF does not…but for a new user that’s certainly non-obvious.

Hence, VALUE_FLAG_UNEVALUATED was born

I had the idea that values would have a bit on them that would say whether they were the product of an evaluation, or just literal. Hence when IF ran, it could tell the difference between:

block: [a b c]
if block [...]

and

if [a b c] [...]

The former it would allow, the latter it would prohibit with an error. This seemed like a good thing.

Does it do more harm than good?

You probably shouldn’t write if [literal block] in your code (unless you were writing a test, or plan on transforming it before execution). Yet you might well want to do this in a composition.

 compose/only [... if (conditional-expression) [...] ...]

If that conditional expression is a literal block, then do you want to get an error on it?

And really, what if someone does write:

 condition: [var = value]
 ...
 if condition body

If they intended that condition to execute, that’s a mistake that wouldn’t be caught. So here we have the check impeding experts…and not protecting new users as thoroughly as one would hope.

Nearly every safety feature has shown to be not worth it

Because Rebol is subtle and can be difficult to debug, it has been tempting to throw in various safety mechanisms. But nearly all that have been tried have been thrown out, instead addressing the relevant concern through a different angle.

There’s no question that the existence of VALUE_FLAG_UNEVALUATED and maintaining its state makes the evaluator code more complicated. Yet I’ve found being able to read it as being helpful in some cases when debugging or deprecating APIs. It’s likely good to keep the feature and let functions know this property of their callsites if they find it necessary.

But should things like the protection of IF and other conditional clauses be in the box, or should it be part of an adapted definition in a “training wheels” module…not enshrined in the mainline code? (For that matter, should the website demo use training wheels mode by default, if it existed?)

I think I’m leaning toward making this a training-wheels-only feature, and having the stock IF (CASE/etc.) not error on literal blocks. Of course, the training wheels would have its work cut out for it to emulate every conditional test in a CASE statement, it would basically have to reimplement CASE. So this is a fairly heavy change.


#2

Hm. I think newbies might get tripped up on this temporarily or accidentally, but it doesn’t seem like a feature that should be changed because a newbie hasn’t yet grasped something as fundamental as literal values.


#3

Another area this has caused problems is in tests. You want to write (<foo> = if [1] [<foo>]) but can’t because you’re being “protected”.

There are definitely some real, useful, power-user applications of the unevaluated flag. This leads me to not want to rip it out entirely. Some functions get real benefit from being able to tell if the callsite was 10 or 5 + 5 and it makes a difference they can action on…helpful in dealing with interface changes, for instance.

But building it into conditional tests in the system itself is probably a mistake. What it’s already done is introduce two tests: IS_TRUTHY() and IS_CONDITIONALLY_TRUE() (and by extension, IS_FALSEY() and IS_CONDITIONALLY_FALSE()). Then you have to have explanations that say “well, it disallows literal blocks, which you don’t want to do in some places for sure but it’s thought to be helpful in some interfaces”.

This flag was tried for a while as a solution to mitigate non-block branches, and scrapped there as well. Because non-block branches were figured as a bad tradeoff all told. So I think IS_CONDITIONALLY_TRUE()
and IS_CONDITIONALLY_FALSE() in the core needs to go.

I still want to be thinking about how one might build the training wheels layer and use the bit, so we’ll put a pin in that.


#4

I’ve axed it, and I want to make the point that these things aren’t necessarily hard to do or change, more just hard decisions to make:


#5

Food for thought, is the MAYBE construct, but it’s written in usermode:

>> x: 10

>> x: maybe case [1 > 2 [<nope>] 3 > 4 [<nope>]]
>> x
== 10

>> x: maybe case [1 > 2 [<nope>] 3 < 4 [<yep>]]
>> x
== <yep>

MAYBE is similar looking to DEFAULT, so similar that you might think it requires a BLOCK! on its right. But while DEFAULT conditionally runs the code on the right, MAYBE always runs it. So a BLOCK! on the right as a literal would undermine it, as the MAYBE would always do the assignment.

>> x: null
>> x: default [1000 + 20]
>> x
== 1020

>> x: maybe [case [1 > 2 [<nope>] 3 < 4 [<yep>]]
== [case [1 > 2 [<nope>] 3 < 4 [<yep>]]] // whoops

For this reason it put in a protection against literal uses. That has really been helpful, because it’s easy to get wrong. I definitely do make that mistake, and I wrote MAYBE–so I’m sure other people would.

This gives me second thoughts, just about whether “in the box” constructs should be protecting or not. Going through with the removal for IF/CASE/etc. showed it wasn’t really as invasive as I might have thought…fairly few changes needed.

Perhaps this is an instance where erring on the side of the checks for Beta/One is worth it? Or is the better solve to make MAYBE take a hard-quoted GROUP! and run it even though it doesn’t need to? This was done with UNLESS, to make it appear more consistent with AND and OR.

>> x: maybe case [1 > 2 [<nope>] 3 > 4 [<nope>]]
** Error: MAYBE only takes a GROUP!

>> x: maybe (case [1 > 2 [<nope>] 3 > 4 [<nope>]])
>> x
== 10

>> code: [case [1 > 2 [<nope>] 3 > 4 [<nope>]]]
>> x: maybe code // checking evaluated bit wouldn't error on this
** Error: MAYBE only takes a GROUP!

>> code: [case [1 > 2 [<nope>] 3 > 4 [<nope>]]]
>> x: maybe (code)
>> x
== [case [1 > 2 [<nope>] 3 > 4 [<nope>]]]

That is actually safer. But where does the protection stop? What property of IF makes it not require a GROUP!, while MAYBE does? In this case, the argument might be that MAYBE sure looks a lot like DEFAULT…and with it seeming to be in the same family, it’s easy to screw up.

Put another way: It’s hard to say that MAYBE doesn’t need some kind of protection, and literally quoting its right hand side and expecting a GROUP! there is a way of accomplishing that. But requiring a GROUP! where it’s not necessary has the downside of interfering with COMPOSE operations when it doesn’t have to. Requiring a BLOCK! doesn’t convey the “always executed, not conditionally executed” nature. This makes checking the unevaluated bit an attractive compromise, so if it’s used there why not on conditionals as well?

Thinking about the influences of COMPOSE, I’m feeling like x: 10 unless case […] is rather nice vs. x: 10 unless (case […]). This does get down to asking “who is Rebol for, and do they need more protection, or just a better debugger”.

Note adding the protection in a “training wheels” module is as easy as:

if: adapt 'if [
    lib/if unevaluated? 'condition and [block? :condition] [
         fail "Literal block used as condition of IF"
    ]
]

Should there be a rule that none of the natives/mezzanine do this by default, and that any code which is executed unconditionally never require a BLOCK! or GROUP!? Or is a more nuanced rule needed?


#6

My feeling is that it can be difficult to predict what newbies will struggle with. As long as the built-in help is available and clearly written, ideally with some kind of feedback mechanism available, I think that would provide a quicker way forward than building stuff for an (as yet) imaginary audience of users. :slight_smile: Of course, if your gut tells you that some guardrails are necessary from the outset, then I trust your judgment. If the protection is easy to implement, I don’t think I’d prioritize its inclusion very highly until it becomes a quantifiable compaint.