TO NOT B OR TO NOT (B)...is...no longer a question

Empowered by a wave of recent technical enhancements, a very, very cool change has shown how wide-open the sky is now for expression via enfix. Now you can write things like:

if block? x and (length of x = 1 + 2) [
    print "x is a block of length 3"
] else [
    print "x is either not a block, or not length 3"
]

That's not some bizarre, carefully-crafted and isolated case that only works on a fluke. This is machinery clicking together after quite a lot of exploration. And there's so much "cool" in there I almost don't know where to start...

(UPDATE: a somewhat off-the-cuff experimental trick to try and skip expressions in "neutral" via a DON'T operator turned out to have too many drawbacks for short-circuit evaluation, sadly... hence you do really need a GROUP! or BLOCK! or something around (length of x = 1 + 2) if you're going to short-circuit. But the rest is still true--as per the topic of this post, which is left normal enfix...which has developed over months instead of hours. :stuck_out_tongue: )

In most ways this is strictly more powerful...since the restrained way in which operators were used before generally can still work. But in looking at the tests, I found one "casualty" of all the cool. That is that if not x = y [...] gets interpreted as if (not x) = y [...].

At a mechanical level, this is because changing = to be left-enfix-normal instead of left-enfix-tight means it gives one chance at deferment. That chance is while = is sitting up next in the frame's token pipeline, at the moment x is being filled in NOT's frame as an argument. But when NOT doesn't try gathering any further arguments it doesn't rewind time and try to splice in the =... to the formerly filled argument cell. It "dampens" the deferment--NOT X goes ahead and runs.

That's how this particular cookie bounces. Yet there's no question this should be accepted as a casualty of the change. Reasoning:

  • None of the old mechanics are going away, so someone can have their own "skin" of = which is left-tight, when running in Rebol2/Red mode. This is different from past times discussing radical enfix changes--where a lot of the circumstances were about enfix becoming more "uniform" in a way that would not permit alternatives.

  • When discussing it before, we've pointed out there are probably more good options in Rebol than in other languages: if x <> y [...], unless x = y [...], if not (x = y) [...]

  • I'm not even so sure that the most natural-looking interpretation of not x = y is not (x = y) in the first place. Most languages which operate on a precedence model (which this isn't using) would say (not x) = y. That includes C... !x == y

So given the necessity of the change, all that's left to talk about is what more cool things to do...to help people migrate and appreciate it.

I'm definitely thinking more about ways of "sensing" code when changes happen. We might imagine a hooked NOT that can ask "hey, am I feeding my result into the left side of an equals?" and put a warning in a log. A lot of those mechanisms seem like they are part of the debugging and introspection API, or hooks into the evaluator loop itself...so I do want to be thinking about that.

I thought I'd tack on a brief history of enfix and how we got to this point...


(Not so) Long ago, there was a push-and-pull regarding how infix should work

While some people imagined R3-Alpha used a simple rule (along the lines of "infix functions run left-to-right"), infix execution didn't exist in isolation. Behavior--including errors--had to be defined for any combination of infix and prefix functions. The actual mechanic didn't work how people thought it did.

I was motivated to change the status quo for several reasons:

  • generalization to arities greater than 2: Imagine an operator that just chooses to discard its left argument entirely. Would you then expect a enfix-op b c d e to act any differently than prefix-op b c d e? R3-Alpha treated the single right-hand-argument of an op differently, and this wreaked havoc on most of my interesting ideas for "enfix".

  • hard to comment what was going on: It was pretty easy to break the R3-Alpha code for processing left and right hand sides of OP!, and not notice until some math test broke later on. I tried naming variables things like "lookahead" or "lookback", and mechanically characterizing them, but it was hard to put an underlying rationale on it.

  • I perceived it as more uniform with prefix: If looking at it from a clean slate, one could reasonably argue someone familiar with Rebol's prefix forms would find it more sensible for 1 + 2 * 3 to act like add 1 multiply 2 3. It was simpler code and it made the evaluation more "regular"

  • I didn't think anyone was all that attached to the status quo: Any new user confused by 1 + 2 * 3 being 7 would not be complaining because it acted like add 1 2 multiply 3. They'd complain when 1 * 2 + 3 acted like multiply 1 add 2 3 and ignored their idea of precedence, and they'd complain about that no matter which way the enfix leaned.

Amidst changes and trials and simplifications @MarkEye and I went back and forth on it constanty. One particularly curious argument he had was to say that yes...R3-Alpha's infix behavior was less consistent with the prefix-parallel I was proposing--but having the two modes was a way of adding expressivity and choice.

Indeed, the desire for that choice came up with one of my own proposals...which was to turn / into a kind of generic pathing operator. But if you write:

a / b / c

To get semantics compatible with a/b/c that has to be seen as (a / b) / c. So I had to admit the ability to "greedily" process "left-to-right" was necessary. Ultimately the settlement was to create a new parameter class, which we called #tight...which joined :hard-quoted and 'soft-quoted and plain-old WORD! normal arguments.

Infix math operators like + and * kept their #tight-parameter ways, while I was free to experiment with what the left-hand side of a "normal" enfixed operation would look like. Various pitfalls were discovered in the process, such as @giuliolunati finding that a greedy completion of the left hand side would ruin things like return if ... [...] else [...].

This eventually toned down the idea of left-hand side completion to "a single expression's worth of deferment".
One last usability issue was just fixed yesterday using a kind of time-traveling trick, to have the evaluator optionally re-enter an argument it had previously punted on in order to avoid an error.

So I think it's come out more or less as Mark said, that more expressive choice is good. Yet I don't want this to become some kind of deferment-arms-race. We don't want @undeferrable arguments and pick functions like NOT to be able to specially overpower =. :frowning: If someone is that bothered by it, and doesn't like the name UNLESS, they can always make an if-not function.

Once again, long story is long...but the endpoint just being that I think we've found a pretty good balance of expressivity with the forms we have, and I'd like to see them being hammered out further before adding any more.

1 Like