Compatibility MAP-EACH (and problems therewith)

In trying to make a shim where MAP-EACH splices and has /ONLY, I thought the easiest way of doing it might be to redo it in terms of a COLLECT of a FOR-EACH.

This delves some into the ambition of Ren-C to raise the bar for Rebol, so it raises some questions. First, let's try a naive approach:

map-each: function [
    {https://forum.rebol.info/t/1155}
    return: [block!]
    'vars [blank! word! block!]
    data [any-series! any-path! action!]
    body [block!]
    /only
][
    collect [
        for-each :vars :data [
            keep/(only) do body
        ]
    ]
]

One reason this won't work correctly is because BODY is executed via a "link" of DO instead of being embedded into the body of the FOR-EACH. That means it won't bind to the VARS variables. And in a definitional-break-and-continue world (which I've been considering) it won't have words like CONTINUE or BREAK bound.

We can address that by embedding the code in, let's say we just put it in as a GROUP!

    collect [
        for-each :vars :data compose [
            keep/(only) (as group! body)  ; only inside path, not a compose/deep
        ]
    ]

This simple implementation supplements the body of the MAP-EACH with additional code. It does it by composing in the body code as a GROUP!, so that it will pick up any bindings the FOR-EACH would add.

Fair enough, but it has a couple of problems. One problem: what if I said map-each keep [1 2 3] [...]? :-/ Our supplemental body adds code that the user doesn't see at the callsite, so they don't know to avoid usage of words that are in that supplemental body for their loop variables. This gets worse the more supplemental code you have.

I think we need to expose something lower-level than FOR-EACH

Really it seems like what you need here is a tool that lets you set up whatever binding object a loop is going to use, gives you a chance to bind code to that object, then lets you run the iteration independent of binding. Something like:

    collect [
        [context looper]: make-loop-stuff :vars :data
        bind body context
        while [looper] [
            keep/(only) do body
        ]
    ]

The imagined MAKE-LOOP-STUFF would give you two things back: a context to bind any code into that you wanted to see the changes to variables in, and a function you could call that would update the values of those variables as long as there was more data.

....Just another epicycle of the binding problems...

Binding in Rebol will always be Rube-Goldberg-like, and so the question is how to maximize the fun and minimize the annoyance, while still getting decent performance. I think if people can think of FOR-EACH as a higher level "macro" which makes a lot of assumptions in order to be ergonomic, they can realize that writing their own loop is going to involve digging deeper.

Something like the pattern above could be used to implement FOR-EACH, MAP-EACH, or REMOVE-EACH...though they could retain their native optimized versions. There's still worries about mutating the bindings on passed-in code (the bind body context above) so a "good" answer would be something like body: in context body where that was understood to not modify the original, but give a rebound "view" at a lower cost.

    [context looper]: make-loop-stuff :vars :data
    bind body context
    while [looper] [
        keep/(only) do body
    ]

I'll point out that this would be cleaner if LOOPER could be both a function and an object. This might suggest being done with FRAME!:

    looper: make-loop-stuff :vars :data
    bind body looper
    while [do looper] [
        keep/(only) do body
    ]

But the current idea of frames is that they expire after you execute them, since it is generally understood that Rebol functions can mutate their arguments; so once a function finishes it may have completely trashed its state so it can't run again. You can DO COPY of a FRAME! however, but then there's no way to have frames preserve state across multiple invocations. :-/

Functions can accrue state by means of referencing some external object where the state lives. But there's not any meaning currently to binding to a function. If you could:

    looper: make-loop-stuff :vars :data
    bind body looper
    while [looper] [
        keep/(only) do body
    ]

JavaScript lets functions act as objects. Rebol would have a bit of a conundrum with that, because pathing on functions is used to specify refinements...not members. But this is an interesting case where being able to let a function expose some properties (the vars data of the internal state) would be of use.

You could also have the function have some mode where it gives back the object where its internal state lives:

    looper: make-loop-stuff :vars :data
    bind body looper/state  ; /STATE would be a refinement
    while [looper] [
        keep/(only) do body
    ]

Not quite as elegant, but could work.

Various things worth thinking about. :-/

MAKE-LOOP-STUFF could perhaps be called ITERATES:

looper: iterates :vars :data

This works for the variables, but raises the question of meaning for BREAK and CONTINUE.

Today's BREAK and CONTINUE climbs the call stack and looks for a loop that's listening for it, which means the while [looper] would react to them as expected. But imagine a hypothetical variant MAP-TWICE:

>> accumulator: 0
>> map-twice x [1 2] [
       accumulator: accumulator + 1
       x + accumulator
   ]
== [2 3 5 6]

You might implement MAP-TWICE like:

collect [
    looper: make-loop-stuff :vars :data
    bind body looper/state  ; /STATE would be a refinement
    while [looper] [
        loop 2 [keep/(only) do body]
    ]
]

Now what happens when you BREAK? The BREAK would not break the entire MAP-TWICE operation as desired, but just the LOOP.

The BREAK-is-the-only-way-to-get-NULL protocol for loops helps here:

    while [looper] [
        loop 2 [keep/(only) do body] else [break]
    ]

And CONTINUE happens to work incidentally; but the fact that you can't tell from outside a loop if it continued or not is almost certainly a problem for constructs that have an unusual concept of what CONTINUE means.

I'm sure there's some CATCH-based answer that could let people rig something up to manually control the reactions, but what I'm mulling over more is the implications for definitional BREAK and CONTINUE. Since the looper is not on the stack, there's no way to jump up to it. You'd have to make the looper implicitly do the code:

looper: make-loop-stuff :vars :data
bind body looper/state  ; would include BREAK and CONTINUE
looper [
    keep/(only) do body
] else [
    ; stuff to do if something in BODY ran a BREAK
]

You could wire in some cleanup code that would happen on CONTINUE or BREAK by hooking the functions in LOOPER/STATE/BREAK and LOOPER/STATE/CONTINUE.

Beyond other reasons of "just being cool", I think that this ability to tailor the BREAK and CONTINUE in customized loop constructs is looking like a very strong argument for making them definitional; e.g. specific functions generated for each loop that know which loop it is supposed to be broken or continued.

1 Like