4

In Rebol, there are words like foreach that allow "block parametrization" over a given word and a series, e.g., foreach w [1 2 3] [print w]. Since I find that syntax very convenient (as opposed to passing func blocks), I'd like to use it for my own words that operate on lazy lists, e.g map/stream x s [... x ... ]. How is that syntax idiom called? How is it properly implemented?

I was searching the docs, but I could not find a straight answer, so I tried to implement foreach on my own. Basically, my implementation comes in two parts. The first part is a function that binds a specific word in a block to a given value and yields a new block with the bound words.

bind-var: funct [block word value] [
  qw: load rejoin ["'" word]
  do compose [
      set (:qw) value
      bind [(block)] (:qw)
      [(block)] ; This shouldn't work? see Question 2
  ]
]

Using that, I implemented foreach as follows:

my-foreach: func ['word s block] [
    if empty? block [return none]
    until [
        do bind-var block word first s
        s: next s 
        tail? s
    ]
]

I find that approach quite clumsy (and it probably is), so I was wondering how the problem can be solved more elegantly. Regardless, after coming up with my contraption, I am left with two questions:

  1. In bind-var, I had to do some wrapping in bind [(block)] (:qw) because (block) would "dissolve". Why?

  2. Because (?) of 2, the bind operation is performed on a new block (created by the [(block)] expression), not the original one passed to my-foreach, with seperate bindings, so I have to operate on that. By mistake, I added [(block)] and it still works. But why?

ftl
  • 647
  • 3
  • 13
  • If this is the kind of topic that interests you, I definitely suggest joining [discussions on the forum](https://forum.rebol.info/t/separating-parse-rules-across-contexts/313/4?u=hostilefork) or the [chat room here](https://chat.stackoverflow.com/rooms/291/rebol)! – HostileFork says dont trust SE Nov 11 '17 at 01:03
  • 1
    Thank you for the invitation (and your great answer, you even spotted my edit ;))! I am very much interested in these kinds of things, so I will join you. Thanks again! – ftl Nov 11 '17 at 08:46

1 Answers1

4

Great question. :-) Writing your own custom loop constructs in Rebol2 and R3-Alpha (and now, history repeating with Red) has many unanswered problems. These kinds of problems were known to the Rebol3 developers and considered blocking bugs.

(The reason that Ren-C was started was to address such concerns. Progress has been made in several areas, though at time of writing many outstanding design problems remain. I'll try to just answer your questions under the historical assumptions, however.)

In bind-var, I had to do some wrapping in bind [(block)] (:qw) because (block) would "dissolve". Why?

That's how COMPOSE works by default...and it's often the preferred behavior. If you don't want that, use COMPOSE/ONLY and blocks will not be spliced, but inserted as-is.

qw: load rejoin ["'" word]

You can convert WORD! to LIT-WORD! via to lit-word! word. You can also shift the quoting responsibility into your boilerplate, e.g. set quote (word) value, and avoid qw altogether.

Avoiding LOAD is also usually preferable, because it always brings things into the user context by default--so it loses the binding of the original word. Doing a TO conversion will preserve the binding of the original WORD! in the generated LIT-WORD!.

do compose [
    set (:qw) value
    bind [(block)] (:qw)
    [(block)] ; This shouldn't work? see Question 2
 ]

Presumably you meant COMPOSE/DEEP here, otherwise this won't work at all... with regular COMPOSE the embedded PAREN!s cough, GROUP!s for [(block)] will not be substituted.

By mistake, I added [(block)] and it still works. But why?

If you do a test like my-foreach x [1] [print x probe bind? 'x] the output of the bind? will show you that it is bound into the "global" user context.

Fundamentally, you don't have any MAKE OBJECT! or USE to create a new context to bind the body into. Hence all you could potentially be doing here would be stripping off any existing bindings in the code for x and making sure they are into the user context.

But originally you did have a USE, that you edited to remove. That was more on the right track:

bind-var: func [block word value /local qw] [
    qw: load rejoin ["'" word]
    do compose/deep [
        use [(qw)] [
            set (:qw) value
            bind [(block)] (:qw)
            [(block)] ; This shouldn't work? see Question 2
        ]
    ]
]

You're right to suspect something is askew with how you're binding. But the reason this works is because your BIND is only redoing the work that USE itself does. USE already deep walks to make sure any of the word bindings are adjusted. So you could omit the bind entirely:

do compose/deep [
    use [(qw)] [
        set (:qw) value
        [(block)]
    ]
]

the bind operation is performed on a new block (created by the [(block)] expression), not the original one passed to my-foreach, with separate bindings

Let's adjust your code by taking out the deep-walking USE to demonstrate the problem you thought you had. We'll use a simple MAKE OBJECT! instead:

bind-var: func [block word value /local obj qw] [
    do compose/deep [
        obj: make object! [(to-set-word word) none]
        qw: bind (to-lit-word word) obj
        set :qw value
        bind [(block)] :qw
        [(block)] ; This shouldn't work? see Question 2
    ]
]

Now if you try my-foreach x [1 2 3] [print x]you'll get what you suspected... "x has no value" (assuming you don't have some latent global definition of x it picks up, which would just print that same latent value 3 times).

But to make you sufficiently sorry you asked :-), I'll mention that my-foreach x [1 2 3] [loop 1 [print x]] actually works. That's because while you were right to say a bind in the past shouldn't affect a new block, this COMPOSE only creates one new BLOCK!. The topmost level is new, any "deeper" embedded blocks referenced in the source material will be aliases of the original material:

>> original: [outer [inner]]
== [outer [inner]]

>> composed: compose [<a> (original) <b>]
== [<a> outer [inner] <b>]

>> append original/2 "mutation"
== [inner "mutation"]

>> composed
== [<a> outer [inner "mutation"] <b>]

Hence if you do a mutating BIND on the composed result, it can deeply affect some of your source.

until [
    do bind-var block word first s
    s: next s 
    tail? s
]

On a general note of efficiency, you're running COMPOSE and BIND operations on each iteration of your loop. No matter how creative new solutions to these kinds of problems get (there's a LOT of new tech in Ren-C affecting your kind of problem), you're still probably going to want to do it only once and reuse it on the iterations.