Letting Go of Enfix Path Dreams (but fixing it with "Magic")

When first looking at Rebol's source code I thought it strange there weren't "infix PATH!s". A WORD! would run either a prefix function or an infix function. But not a PATH!. There was a place where it would be, and it was just an error.

One might argue whether enfix functions "actually need" refinements. But consider instead....picking operators out of contexts seems an uncontroversial need. Here's a survey of responses to that case: Rebol2 gives a cryptic error, R3-Alpha gives a slightly less cryptic error, and Red brings its own, uh..."personality":

rebol2>> o: make object! [plus: :+]

rebol2>> 1 o/plus 2
** Script Error: plus expected value2 argument of type: number pair...

r3-alpha>> o: make object! [plus: :+]
== make object! [
    plus: op!

r3-alpha>> 1 o/plus 2
** Script error: op! type is not allowed here

red>> o: make object! [plus: :+]
== make object! [
    plus: make op! [[
        "Returns the sum of ...

red>> 1 o/plus 2
== 2

But it's legitimate to want this. For instance: in the %httpd.reb code, there was a variable holding the "HTTP Method", which collided with the new enfix METHOD. But since METHOD quotes the left hand side to know where to bind into, you can't just say my-meth: lib/method [...] [...] and have it work.

...but I have an alternative idea...

I'll explain why I don't think undecorated paths should dispatch enfix in a moment. But first, my alternative solution, which is basically 99% as good...yet actually (I think) feasible:

Give the user an operator that can sense the evaluative semantics needed of the left hand side, guided by the right, to explicitly declare an enfix dispatch, e.g.

 my-meth: -> lib/method [...] [...]

I'm suggesting using the right pointing arrow for "pushing" the left hand side into the right. It kind of goes along with the current use of a left-pointing arrow for packing up the right side and pushing it to the left...which is also enfix-related.

This operator would rely on some weird magic that doesn't exist yet. It would have to decide its left hand side parameter convention after it was already running and looking at its right hand side. Really it is like a function with a left-hand variadic side, where during invocation it can actually cause a varying amount of code on the left to execute based on partial evaluations of the right, which it then runs more of after. If such mechanics were used carelessly they could really confuse left-to-right ordering...though FYI, you can see Rebol is already not purely left to right.

You might want to use this operator even with non-enfix functions, to effectively enfix them off-the-cuff:

>> 5 + 5 -> subtract 7
== 3

So why is this better than just implementing enfix pathing?

You can probably understand by looking at a very simple case from the tests:

e: trap [do make block! ":a"]
e/id = 'not-bound

But don't read it on two lines. Read it on one.

e: trap [do make block! ":a"] e/id = 'not-bound

Now imagine that you are the evaluator. You've got a SET-WORD! for E: pending on the stack, but haven't assigned it yet. You've also got a WORD! for invoking the trap action on the pending on the stack, but you haven't invoked that yet either. Instead you're gathering TRAP's argument to run.

You've just seen the BLOCK!. Now you notice a PATH! of e/id pending. What you want to know is if that's some kind of enfix operation that has an interest in influencing the operation. It might want to quote the block, or it might want to force some kind of completion of the left hand side.

But E could be anything, here. e could be a DATE!, a STRUCT!, an IMAGE! or other user defined type. Yet somehow we expect the path dispatch process to give back a meaningful answer about its interest in code to its left.

  • In this case, whatever we learn in this path dispatch we're going to have to throw away. E is going to change.
  • While word lookup is done through a system mechanism with limited sources of errors, the path dispatch code is arbitrary and can generate errors for pretty much any reason. Trapping those errors is not cheap in C-like systems: each setjmp() to wrap them costs something. All for a path evaluation you're going to have to run again (though you wouldn't need an error trap, since you'd be willing to have it actually fail).
  • Imagining that you even could cache the results of a first path evaluation to reuse again (how would you know?), the dispatch is imperative code. Asking it to guarantee it returns the same result every time could be error prone even if it was a desirable goal.
  • Plus on top of that absence of guarantee, you can be sure that if any user GROUP!s were used it wouldn't promise to return the same thing a second time.

One might propose some sort of limited, light PATH! evaluation which "only works for some types". So it could recognize picking an ANY-CONTEXT!. But that sounds...bad. Fewer flaky workarounds are needed, not more.

What does the operator buy us?

It gives explicit advance notice that you think the thing on the right is an ACTION! (and can error if it isn't).

It gives permission to do the path evaluation once-and-only-once on the right hand side, prior to making decisions about parameter conventions on the left. (Arguably it could be giving permission to evaluate the right before any remaining argument gathering on the left.)

The right could also be a WORD!, or maybe a GROUP!

1 + 2 -> (either x [:subtract] [:add]) 7

An operator of this type is inspired by developments on ME and MY, as well as the advancements in MATCH. It's trickier than those...which are still somewhat experimental themselves. But I think it is doable, and it seems like time to set the course for this and forget about undecorated enfix dispatch.

If a path is executed and looks up to an enfix function without the operator, it should error as it does today. But it should tell you about the operator so you can use it!

This operator now has a first-cut implementation! I've named it "SHOVE"

I'm really quite pleased with this direction. While this wasn't on any near-term roadmap, not knowing what to do about enfix paths was a rather dark cloud hanging around...ever since I first saw R3-Alpha's source.

Something additional that I thought of is that not having paths automatically dispatch enfix can be argued for more effectively in Ren-C, because there is no such thing as an enfix function. It's a property of words and bindings. And as it happens, when you use a PATH! to do lookup, you aren't using a word binding...the binding is ignored, and only the value of the word is used.

It's going to take a while for it to shake out and all the quote prioritization issues to settle. Also, the current implementation doesn't use a generic mechanism...e.g. you couldn't write SHOVE yourself in usermode...yet. But I think that it should be possible--and that this mechanism is variadic left enfix. Which means that although some evaluation has been performed on the left, the item immediately to the left hasn't (like a hard quote)...and the variadic gets the opportunity to pick the parameter convention to use when asking for the left argument.

I should note that the shove operator (and left enfix variadics, generally) produce something that is not a pure left-to-right experience. But historical Rebol already was not purely left to right, e.g. SET-PATH!s don't evaluate their own target location until after the right hand side:

1 Like