Right Literal Args vs. Left Literal Args... Fight!

One might think it obvious that an ordinary function that takes its first argument literally needs to "win" taking its argument to the right... even if an infix function after it gets its left argument literally.

You might think this because HELP needs to work:

>> help ->
USAGE:
    @(spec) -> @(body)

DESCRIPTION:
    Makes an anonymous function that doesn't copy its body, can unpack args...

That makes it seem HELP taking its right hand side literally has to win...therefore you couldn't use HELP as the name of a variable to make arrow functions, e.g.

weird-increment: help -> [help + 1]  ; same as `lambda [help] [help + 1]`

If HELP always won looking right, that would not be legal.

But Left-Taking-Right-Literally Can't Get "First Dibs"

Consider the following from the evaluator's point of view:

(print "message", $lib)/help ->

Let's pretend for a moment that the evaluator gives the left hand side the first shot. In that world, if the left is looked up and doesn't take the right literally, then the right hand side gets second dibs...and is checked if it takes its left side literally.

But notice that to get the knowledge of if left-quotes-right or not, you have to evaluate that path. That can do processing which has costs you might not want... and groups can have side-effects (like the printing of the message). But since infix dispatches from words only, it doesn't have that problem; it can be checked "cheaply" with no side-effects, making what the right hand side wants of the left a good fit for the first thing to check...falling through to the expensive operation with side-effects that you then know wins.

So that's why I've been letting right quoting over left win. But you might ask: how can HELP work without making you say help '-> (which might lead down the road of consistency and making you type help 'append) ? Because there's been a rule: "Right operators that are left literal get priority, BUT become inert if they have nothing following them."

help ->                                ; gives you the help for lambda
help -> [print ["argument is" help]]   ; gives you an arity-1 function 

Weird, but it seemed to work well enough.

Reviewing it now, it's somewhat limiting. It means you can't have postfix operations that take their left hand sides literally taking non-inert items on their left. But the alternative would mean never allowing left-literal paths, or having dodgy properties when you do. Hm.

Can It Be Narrower?

Might only "escapable literal" @(left) parameters have this loophole, with full (@literal) operators being presumed rare enough to break HELP...and still run at end?

>> ?: infixed func [@left] [print ["got" mold left]]

>> _ ?
got _

>> help ?
got help

>> help ($?)   ; HELP is `@literal` but evaluates groups
? is an ACTION!
...help text here...

>> help $?   ; HELP would allow this too
? is an ACTION!  ; and ACTION! now carries labels, so it can know the "?"
...help text here...

To me, this seems to give a number of options to someone who wants to get HELP on a left literal infix operation. Enough options to not make left-quoting infix of words and paths impossible.

And how bad would this be, really? If we narrowed it down to where the only operators that require escaping to get HELP are those that are genuinely postfix hard literal with no arguments... that's a pretty small set. To date, we have ZERO such operators in the box, they only exist in tests.

This is important!

Having these things work out is the point, because this is what the unique offering is. These issues all lock together on foundational questions e.g. those pointed out by "speaking with tics".

When left-literal infix operators can subvert literalism from the right, that means when you say for-each x you can't be sure that X won't act on the FOR-EACH. But we can see that as different from the situation type of is in...since OF gets the first shot.

Does that suggest the "out-of-the-box" constructs always have you decorate things you want literally on the right? It doesn't seem we'd want to bear the consequences of that on HELP...

>> help append
** Script Error: append is missing its series argument

 >> help $append
 ; this would work...

Just have to keep everything all lined up.

Above I explain some of why right-looking-left has to get priority over left-looking-right in a direct "fight" of trying to take each other literally.

But what if they're not trying to take each other literally, but the contention is that they are both "fighting" over the same value between them..


Here are two functions. One takes its right-hand side literally, and the other takes its left-hand side literally:

grab-right: func [@right] [...]
grab-left: infix func [@left right] [...]

What happens when I write:

 grab-right X grab-left Y

(I deliberately gave GRAB-LEFT a right-hand argument...just to make clear we're not talking about a situation with the "nothing on the right" exemption that allows help -> to work)

Seems like an unbreakable tie. :necktie:

You're going to be lying to someone if you don't trigger an error. It could be a pretty big lie...e.g. if you let one evaluate and pass their result to the other, then you might be giving an antiform to a function that specifically only expected source-representable things.

But what if the situation was a bit more...malleable...

Let's say that one of these lets you escape the argument inside a GROUP! (currently thinking the most semiotic way to represent such a property of a parameter is by...putting it in a group).

want-right: func [@(arg)] [...]  ; `want-right ('X)` acts like `want-right X` 
grab-left: infix func [@left right] [...]

So this means that WANT-RIGHT is geared up to accept evaluative products. If that's the case, then we might think of ourselves as having a bit more liberty to tiebreak:

want-right X grab-left Y
=>
want-right (X grab-left Y)

Whether that seems presumptuous or not to you, it found a use with arrow functions:

case [...] then x -> [...]

As long as THEN takes its argument as an escapable literal (and not a literal), this works.

The interface on the function says it was "willing to accept a group at the callsite". But it never actually received a group, just the evaluative product, which would then be typechecked. Given that it's willing to take that evaluative product, why not throw in the group implicitly vs. giving an error?

Another Case Study: Infix "OF"

First there was (type? x) in Rebol2

Then (type-of x) emerged, in the attempts to purge the blight of "functions that return a result end in question mark", reserving it for LOGIC-bearing functions.

It then became (type of x) to be even more pleasing to the words-separated-by-spaces aesthetic.

But the infix properties of such a left-literal function can't be the same as ordinary evaluative infix, e.g. math:

>> 1 + 2 * 3
== 9  ; e.g. (1 + 2) * 3

Because we used to be able to write:

>> integer! = type-of 1
== ~okay~  ; anti

So we'd still like to be able to write:

>> integer! = type of 1
== ~okay~  ; anti

Hence that can't be interpreted as (integer! = type) of 1

"Easy enough" you say... "make it so literal left infix wins over evaluative right infix."

If you think it's easy, try writing this stuff yourself. But yes--that is what I did.

However, OF is one of these "escapable" routines. We want to be able to do this:

>> integer! = (first [type length]) of 1
== ~okay~  ; anti

So how does this work out with our tie-breaking?

It doesn't count. There's no tie to break. = isn't taking its right literally. If it was inescapably taking its right literally, then the escapable left would mean the OF would yield to the hard rightward literal.

1 Like

Well, so much for that, it no longer applies because...

Branches Are Now "Hard Literal" (for good reasons)

:thinking:

This doesn't necessarily mean the feature needs to be thrown out.

But the argument for having some weird edge case in the system is greatly diminished.

The Feature Deviates @arg and @(arg) w.r.t. Infix

My impulse is to kill the feature specifically because it seems like it gives you an unintended consequence when you change a literal argument between being escapable and not.

You might think "oh, it took arguments literally before and didn't take GROUP!s as a legal type. I changed it to take escapable literals and so this should not make any difference to the code until I add groups."

But if there's this other behavior then that's not true. Suddenly it's injecting evaluator sublevels for literal arguments to do infix "lookback".

Also, this is a performance hit. It's better if you just take the slot literally in step one, and then evaluate the group in step two, and consider yourself done.

At Least Three Strikes, And I Think It's Out

  • No Longer Has Any Use Cases I Know Of
  • Creates Undesirable Deviations Between Literal And Literal Escapable Args
  • Slower and More Complex Code

This thread can serve as a memory of what the feature did. And maybe some super-compelling case to resurrect it will come along someday. But when it's resurrected, it should probably be its own special weird parameter flag, instead of being implied merely by the escapability of a literal parameter.

Here Was A Comment on What's Being Deleted...

:bomb:

// Left literals from the right already "win" over rightward literals,
// in a case like `help grab-left` where they point at teach other.
// But there's also an issue where something sits between quoting
// constructs like the `x` in between the `else` and `->`:
//
//     if condition [...] else x -> [...]
//
// Here the neutral `x` is meant to be a left argument to the lambda,
// producing the effect of:
//
//     if condition [...] else (x -> [...])
//
// To get this effect, we need a different kind of deferment that
// hops over a unit of material.  Soft literals are unique in that they
// mean we can do that hop over exactly one unit without breaking
// the evaluator mechanics of feeding one element at a time with
// "no takebacks".
//
// First, we cache the literal argument into the frame slot.  This is
// the common case of what is desired.  But if we advance the feed and
// notice a literal infix construct afterward looking left, we call
// into a nested evaluator before finishing the operation.

So here are some tests that used to work that fail with this gone.

want-right: lambda [@(x)] [compose [<@(want-right)> was (x)]]

want-left: infix lambda [@(y)] [compose [(y) was <@(want-left)>]]

>> want-right foo want-left
== [<@(want-right)> was [foo was <@(want-left)>]] 

Becuase they're both @("escapable") and not @hard-literal, the tie was broken by letting the right hand side (looking left) run first. It got foo literally and produced a BLOCK! which was passed to the left hand side in lieu of foo.

It still works if the right side took its left took hard literally:

want-right: lambda [@(x)] [compose [<@(want-right)> was (x)]]

want-left: infix lambda [@y] [compose [(y) was <@(want-left)>]]

>> want-right foo want-left
== [<@(want-right)> was [foo was <@want-left>]] 

I feel a little more sympathetic towards the idea that maybe this should work as a behavior:

want-right: lambda [@(x)] [compose [<@(want-right)> was (x)]]

want-left: infix lambda [@(y)] [compose [(y) was <@(want-left)>]]

>> want-right foo want-left
== [[<@(want-right)> was foo] was <@(want-left)>]

The reason I'm more sympathetic is that this does not require an additional evaluator level, and it makes something that would just be an error in all cases otherwise do something that may be interesting.

However, it's an asymmetry...and I have a hard time thinking of how it would be useful.

:face_with_diagonal_mouth:

Putting aside "it would be an error if there wasn't a tiebreak" as a justification in its own right...

...when is it that you're looking for patterns like:

   literal-thing |operator| [...]
   (calculation) |operator| [...]

And then someone writes:

   foo (foo-arg) |operator| [...]

And you expect this to be naturally equivalent to:

   (foo (foo-arg)) |operator| [...]

Just because foo took its arg escapably?

It kind of points to the fact that it leaning the other way, it probably didn't make a lot of sense when looked at beyond the arrow functions either.

With grab-right X grab-left Y maybe it seemed reasonable with:

then x -> [...]

But looked at more generically, why would you allow:

foo type of ...

If foo is generally doing

foo literal-thing
foo (calculation)

What gives (type of) the nature to be the kind of thing that shouldn't need a GROUP! around it, just because OF takes its left argument escapably quotably? It doesn't seem to have any correlation at all...the only justification there is it would be an error otherwise, but it seems like an error is better.

Having slept on it, I think the whole thing was unreasonable. Literal contention that is transitive across a value should be an error.

When you look at examples like then 'x -> [...] or then [x y] -> [...] you see that we just wind up having to bend our brain to un-see the natural branching behavior, and a GROUP! being required makes more sense.

Thinking the arrow functions without groups for branches was "neat" blinded me to how dumb this idea really was. And needing strategy for (help ->) made me expect that tiebreaks needed tricky solutions, so I didn't naturally reject it.

Can The Left Be Missing For Left-Literal Arguments?

We "know" right-literal arguments have to be legal to be missing, because we expect HELP with no arguments to work.

>> help
(we expect to see some generic help here)

(There are other ways of accomplishing that--such as the console specially recognizing HELP and noticing nothing is to the right. But for the moment let's assume that if it can be accomplished with the <hole> mechanic, we do it that way.)

But should left-literal looking allow it? :thinking: There might be a good reason to just say no to this.

Notice that under the rules I've articulated, when a function argument is being gathered--and that argument is literal--the "eval stepper" isn't called.

So this means if a function takes three literal args--escapable or not--it will get X, OF, and Y below:

three-literal-args X -> Y

But what if it only took one literal arg?

one-literal-arg X -> Y

Should this act like:

(one-literal-arg X) (-> Y)

In other words, should the -> have the opportunity to see nothing to its left?

You might argue that since there isn't actually "nothing" on the left, the only cases that give you this behavior should require actual separation:

one-literal-arg X, -> Y

one-literal-arg X
-> Y

That makes sense to me.