If ACTION! Combinators Don't Impress, What Will?

Have you ever been parsing and wanted to call a plain old function on a value?

Let's say you are collecting some numbers, and you want to negate them.

>> parse [1 2 3] [collect some [
      num: integer! (num: negate num) keep (num)
   ]]
== [-1 -2 -3]

You're stuck having to name it, transform it, and then (possibly) reference the name again.

UPARSE's mechanics actually make this a bit better already, since GROUP! results can be used directly:

>> parse [1 2 3] [collect some [
       num: integer! keep (negate num)
   ]]
== [-1 -2 -3]

But you still have to cross over into a GROUP! if you want to do any negation, and use a name to move the parsed value into the domain of DO.

Meet A New Trick: ACTION! Combinators

You can pick whatever syntax you like for it (UPARSE is customizable, remember?)

But I'm using paths that start with slash to mean "call a normal function but acquire its arguments via the synthesized result of PARSE rules". (Consistent with the new role of leading-slash in EVAL.)

>> parse [1 2 3] [collect some [
       keep /negate integer!
   ]]
== [-1 -2 -3]

:exploding_head:

You can call any function this way normally by providing each argument in a group.

>> x: 510
>> parse [] [/multiply (1 + 1) (x)]
== 1020

But that's easier read as ((1 + 1) * x). You of course don't want to do it unless at least some of your arguments come from a rule that's not a GROUP!:

>> data: copy ""
>> parse ["a" "b"] [some [/append:dup (data) text! (2)]]
>> data
== "aabb"

I think the primary usefulness is for functions without side-effects where you just want to do a quick transformation on some information you are assigning or collecting.

But that doesn't mean you can't write some interesting machines.

>> data: copy ""

>> parse ["abc" <reverse> "DEF" "ghi"] [
       while [
           /append (data) [
               '<reverse> /reverse /copy text!
               | text!
           ]
       ]
   ]

>> data
== "abcFEDghi"

(Note: with PATH!-based function composition, you should be able to write that as /reverse/copy without the space, and it would be faster...the composition and one ACTION! combinator would be quicker than two ACTION! combinators.)

The foundations of Ren-C are strong, which is why such things can be made. All that stuff about MAKE FRAME! and ADAPT and AUGMENT etc. are about giving control of time and space to the users to build such combinators.

Does This Mean Fewer Combinators Needed?

Anything that doesn't advance the series position doesn't need to speak the combinator interface.

So, for instance, ELIDE.

>> parse ["a" "b" <c>] [collect [some keep text!] /elide tag!]
== ["a" "b"]

But ELIDE is rather useful, so aliasing it as a combinator to invoke it without the slash seems nice. However, this aliasing process should be cheap and easy... to "combinatorize" a function.

But as I say, a plain function doesn't know anything about advancing input and speaking the combinator protocol. What's great is how close I've made combinator protocol to ordinary functions. I'm kind of gloating a lot about it, because it's neat. :slight_smile:

Can We Generalize a Rollback Mechanism?

COLLECT is set up so that if you do some KEEPs in a rule that ultimately fails, the keeps roll back.

One of the things limiting the usefulness of this mechanism for functions with side effects (APPEND as opposed to NEGATE) is that they don't get rolled back on failure.

I've wondered if there could be some kind of DEFER operation which captures GROUP! operations and only runs them if a certain point is reached. Being able to defer any rule that can have side effects might be nice, so that would include these ACTION! combinators. Worth thinking about.

3 Likes

Outstanding... what a performance! You're really outdoing yourself Brian! :clap: :clap: :clap:

1 Like

Maybe overdoing. :slight_smile:

So... it always happens that if you make something that takes no parameters, someone will want to add a parameter...

...and once they can add 1 parameter, they will want to add N parameters, for any N.

...and once they can add N parameters they'll want to be able to variably add (0...N) parameters...

So of course this is the case with combinators. This ACTION! combinator (which runs when a PATH! ends in a slash, and looks up to an ACTION!) is effectively variadic.

Here it is with NEGATE, which takes one argument:

>> parse [1] [/negate integer!]
== -1

Here it is with ADD, which takes two arguments:

>> parse [1 2] [/add integer! integer!]
== 3

Which goes to show you that the combinator for ACTION! doesn't take a fixed number of parsers. Each instance of the combinator takes as many parsers as the function takes arguments.

The Nightmare That Is Variadic Combinators

This really grates against the way the combinators work. Because the body of the combinator only runs once it has been turned into a parser... e.g. all the combinator's arguments have been "combinated out". So all that remains is the input parameter.

To clarify what I mean by "combinated out": If you say some between "(" ")", the fact that BETWEEN takes two parsers is of no matter to SOME. BETWEEN gets "combinated" to where it looks like a parser function that's no different from any other. By the time SOME sees it, the left and right parser parameters have all been wired up--those parameters have vanished. Hence SOME's code is stylized in a way that works just as well for some "a" or any other "fully combinated" parser.

But ACTION! throws a wrench into this. The combinator can't say in advance how many parsers it needs, it depends on the action. And by the time you're running the body of the combinator it is too late...the expectation is that all the "combinating" is done by then.

Making matters more frustrating is that even if you do manage to AUGMENT the interface of something like the ACTION! combinator, the rules are that the base combinator doesn't see those fields. That's a good rule: there is a reason for it. But it means that if parameters get added then there has to be a way to tunnel the augmented frame with the view of the higher level variables down so that when the fulfilled combinator is run as a parser it can see those variables.

I managed to hack around this to implement the first version--and I knew what I was doing was a bit dicey. But the cracks show when trying to design a unified rollback service. The service has to know about all parsers that are parameters...to automatically put in the piping that does rollback.

I've Hacked It Up Again, By Not Using Auto-Rollback

So the ACTION! combinator just goes without the auto rollback mechanism, and manages it manually like the BLOCK! combinator does. But there's no conceptual reason why it would need custom code for its rollback. It's just because the generalized analysis doesn't get a chance to see the parsers and do the automatic thing to them...because the parser parameters are not visible at that phase.

This is going to be a thorn so I thought I'd talk about it. This way everyone knows that although the ACTION! combinators are working, they are on the ropes a bit as being maybe "overdoing it".

But I do like this feature, and am definitely going to try to figure out a better way and keep it alive if I can. This kind of UPARSE feature is powerful and I hate to rule things like it out.

2 Likes

I've updated the post for the new (work in progress...coming soon) syntax. And it sure does look better with the slash in front than the slash at the tail!

>> parse [1 2 3] [collect some [
       keep negate/ integer!  ; great feature, but terminal slash is yuk!
   ]]
== [-1 -2 -3]