Enfix Normal Revisited: Unlimited Default, One-Eval Operator?

When you make a new thing, you wind up with more and more instances of that thing to inform its usage. And we now have a whole menagerie of left enfix normal operations. They are cool, but from using them regularly I notice aspects that are easy to get wrong...and may be hard for people to learn.

Hence it's important--as new information comes to light--to ask if there are improvements we can make. One question I've wondered is the balance of how much to evaluate on the left by default.

We first saw @giuliolunati's "dark corner"...the problem with telling enfix to "just complete":

 return if x < 0 ["a"] else ["b"]

The first time ELSE gets an opportunity to run is when the ["a"] block is sitting to its left. By having a normal parameter type for its first argument (not 'soft-quote, :hard-quote, or #tight), it says it doesn't want to take that block unless it absolutely has to. It wants "some evaluation", if it can get it.

If we take "some evaluation" to mean "as much evaluation as possible", then we're in trouble...because return if x < 0 ["a"] stands on its own. So if you run it, the ELSE never happens! With unlimited left completion, you would have to write:

return (if x < 0 ["a"] else ["b"])

There could also be some kind of operator, which could be a more convenient way of expressing this. Hand-waving the details a bit, let's just call it <-

return <- if x < 0 ["a"] else ["b"]

But if you don't want to have to resort to such a thing, you need a rule that stops the evaluation. This gave rise to the "one expression" limit. It will only evaluate one complete expression, and after that...no more. Since if x < 0 ["a"] is one complete fulfilled expression, it considers itself satiated and runs the ELSE before the RETURN.

When ELSE and company were the only left normal enfix operations, it was always able to complete an IF or UNLESS which provided a left boundary to "wrap" a condition wedged between it and the block. Yet with more operators like OR, AND, THEN...the condition wasn't wrapped. This meant seeing the other side of the coin more often. Consider:

not all [
   something1
   something2
] then [
   print "either something1 or something2 wasn't truthy"
]

Here the limit can bite you. THEN looks to its left, and because it's afraid to consume too much...it stops at the all [something1 something2]. You get:

not (all [
   something1
   something2
]) then [
    print "almost certainly not what you meant!"
]

If we stick with the limit-of-one-rule, you could use parentheses around the (not all [...]) to get the effect. You could also have an operator, but this time it would look like:

not all [
   something1
   something2
] -> then [
   print "either something1 or something2 wasn't truthy"
]

From the point of view of interpreter complexity itself, there's not an obvious winner. The evaluator has to have more or less the same code to support both models.

With completion by default, you can quickly develop a sort of "funny feeling" about passing parameters which use left enfix normal. If someone has been "trained" I think they'd read return if x < 0 ["a"] else ["b"] and think "oh, my mind could complete return if x < 0 ["a"], so the evaluator would too!" Yet it feels a little more hidden to realize that not all [...] then [...] won't work when all [...] then [...] would. Fuzzy argument, I realize.

The stronger point, though, is if we're going to look at the two mitigating operators. I think the <- is easier to explain than the ->. And it seems like it's positioned more in the right place, where it should be. <- is saying "Hi, I'm the left parentheses of an implicit GROUP! whose invisible right parentheses is after the next full expression...including its enfix suffixes." The positioning of -> is weirder, and it's not as clearly a "put this there" kind of thing.

It isn't a major change to the code, but it significantly affects experience. Lately, I've been wanting more complex expressions with my ORs and ANDs and THENs... and hitting it a lot more often than encountering the "dark corner" when it was bent the other way. I have a feeling that letting it complete the left fully...and intervening with a GROUP! or operator when you don't want to...may be the better choice.

I've mentioned before that other languages have introduced operators for avoiding parentheses:

Such operators are generically useful. I feel like left arrow isn't too bad for it:

>> 1 + 2 * 3    
== 9

>> 1 + (2 * 3)
== 7

>> 1 + <- 2 * 3
== 7

This is sort of why I'm feeling more skeptical of the -> alternative above. It's weirder, so the situation motivating its necessity is arguably weirder.

In the case of RETURN, one could theoretically give an error if there's residue hanging off of it (that isn't a comment/invisible):

>> func...[return if 1 < 2 [print "try to return"] else [print "residue!"]]
** Attempt to leave frame with residual code: `else [print "residue!"]`

>> func...[return if 1 < 2 [print "this would work"] comment ["yo"]]
this would work

However... as long as one is willing to introduce a function like <-, is there any real harm in saying that RETURN is another function in this class, and you don't need the <-? Since it doesn't continue execution after the return, and all enfix could be otherwise is a mistake, why not make that implicit?

That will help things that don't return, like THROW. Perhaps the behavior would even be directly tied to an annotation that prohibits the idea that a function returns and continues execution.

But does it make sense for something like APPEND? :-/ Is it more "run the enfix after me" or "wait and let me finish, then run the enfix"?

Really this all comes down to whether the benefit of the nuance in interface is worth its weight in complexity...

Probably would be more straight forward to comprehend. I'd expect more groups in code bases to appear in any case.

Seems reasonable, if RETURN and similar must be at the end of their parent's expression list, then it seems fine that they should take precedence over what remains.

Presumably though that's one expression to evaluate for RETURN's argument, for any more than that an error could be useful.

At some point there's going to be a limit hit somewhere with what one can express without groups while eschewing a precedence system.

Yeah...

1 Like

At some point there's going to be a limit hit somewhere with what one can express without groups while eschewing a precedence system.

This is just a fact. And I don't think we want a precedence system. I had a hard time with even accepting the "tight" parameter convention. :-/

Here's some code that caught my eye in @rgchris's altjson:

either parse json either padded [
    [space ident space "(" space opt value space ")" opt ";" space]
][
    [space opt value space]
][
    pick tree 1
][
    do make error! "Not a valid JSON string"
]

That looked a bit belabored to my eyes, so I wondered if Ren-C could bring anything to the table. I offered this:

; Note: This code has been updated to 2022 terminology

parse json compose [
    ((if padded '[space ident space "("]))
    space opt value space
    ((if padded '[")" opt ";" space]))
] else [
    fail "Not a valid JSON string"
]
pick tree 1

...but it is dependent on ELSE completing its left. If it doesn't, it just completes the COMPOSE.

if RETURN and similar must be at the end of their parent's expression list, then it seems fine that they should take precedence over what remains.

I feel uneasy about the exception. I'd think that if you were forced in these cases to say return <- and received an error as feedback when you did not, that you would be more in the rhythm of understanding the rule. It would "keep your mind in the game".

I feel like <- is pretty easy to understand. People can "get" it pretty quickly ("I want the behavior of a GROUP!, but I only want to specify where the group STARTS, the end is wherever the next expression ends naturally"). And then see <- as being just as effective as a GROUP!'s ( at being the terminal point of an enfix's end-of-the-road, looking leftward.

It has generic usefulness, because often you want to use GROUP! for purposes like COMPOSE...and it's frustrating if that conflicts with your need for precedence in an expression, where the only way to do it is groups. So I think people would find themselves embracing <- for situations they wouldn't have thought of, to save GROUP! for composing (or whatever else that isn't precedence).

The example I cite for full completion here is NOT ALL [...] THEN [...].

It's no longer a good example because THEN was later changed of reacting to truthiness to being non-NULL-reactive. But NOT returns a LOGIC!. So when you complete to the left, the THEN clause always sees either #[true] or #[false] on the left, and runs.

This caused a bug in real code when I was "trying to use it properly" based on the "simpler rule". Sure, I could just change it to AND instead of THEN. But when one of the key examples you use to defend a change fails, it's time to put on the thinking hat back on.

More data means a more informed decision

Throwing in a construct that suddenly "completes everything to the left" may not be as useful as it sounds. I think I've not been as moved by it as I might have thought, nor has it been particularly "simplifying" (as the case above shows).

It may be that the first rule, about completing one expression, was more on the right track. Perhaps not perfect, and we can survey some examples. It also has to be considered with the "annoying" property of last-parameters-of-functions being treated differently.