Re-imagining DO/NEXT (now called EVALUATE)

UPDATE: Because it's confusing for DO to change the result it gives based on a refinement, the /NEXT refinement has been removed from DO...and the following behavior is given by a routine called EVALUATE. So the DO/NEXT suggestion below is now implemented as EVALUATE/SET.

What if DO/NEXT returned the position, evaluated result into var?

e.g. like this:

>> block: [1 + 2 10 + 20]

>> block: try do/next block 'var
== [10 + 20]

>> var
== 3

>> block: try do/next block 'var
== []

>> var
== 30

>> block: try do/next block 'var
== _

>> var
== 30

This has a differentiated step for the end. And if you try to execute a DO/NEXT on the end of a block, you get null back... but it doesn't overwrite the var if you were at the end.

This offers a pleasing aspect to how it might be used in a while loop. You can style the loop to just call DO/NEXT, without checking anything for the TAIL...

while [block: try do/next block 'var] [
    ...
]

And in addition, you have access to the last evaluated value after the loop. Nice, isn't it?

That's using TRY, but since it obeys the null protocol, you're free to do other things...e.g. this code for finding the first non-null evaluated element in an array (something that might be used by a REJOIN-type operation)

until [
    block: (do/next block 'base) else [return null]
    set? 'base
]

Not just "nice", a solution for a nuanced problem (coincidence?)

ELIDE is great, but it has had an unfortunate problem of running as part of the evaluation that precedes it.
The reasoning for the behavior was summarized here. Basically, if you are evaluating a block with a chain of DO/NEXT steps you don't want to get to your last evaluation and realize you've only got invisibles left... when you wanted to evaluate to the last actual result--now thrown away.

But wait... this could be applied to solving that. Because of that last step in the DO/NEXT which returns the null but doesn't overwrite the variable. If there happened to be some residual invisible data vs. just a tail, it works the same way. For instance:

>> block: [elide (print "a") 1 + 2 elide (print "b") 10 + 20 elide (print "c")]

>> block: try do/next block 'var
a
== [elide (print "b") 10 + 20 elide (print "c")]

>> var
== 3

>> block: try do/next block 'var
b
== [elide (print "c")]

>> var
== 30

>> block: try do/next block 'var
c
== _

>> var
== 30

Ta-da. For ELIDE's "lazy" invisible category, this can be made to work. It's pretty nifty that it doesn't interrupt the evaluation stream with anything...it can have side effects, yet the evaluations act like it's not there at all.

This is easier said than done, but I've got a preliminary experiment working--suggesting that I see no reason it couldn't be made to work.

2 Likes

So Much Has Changed In 3 Years. :mantelpiece_clock:

The above was written before ^META values or [multiple returns]: it was a different landscape. Problems that used to seem impossible to solve now seem tractable.

And of course, there are new puzzles introduced by all the ambitious features.

I Still Don't Think DO Should Have /NEXT

DO is quite overloaded as it is. You can just DO a FILE! or URL!, so should DO/NEXT work on that?

In Red, the file %test.r:

Red []

10 + 20
print "Where's my Next?"

Then running it:

red>> do/next %test.r 'pos
Where's my Next?

red>> pos
*** Script Error: pos has no value

It ignored the POS. But I'd say it probably should have just been an error.

DO has enough trouble with things like the /ARGS without tying it in with the concerns of /NEXT. It's probably right to say that evaluation goes in with EVAL.

The Main Result of An EVAL Should be The Result

I thought it would be nice to return the position in many cases, because evaluation is done in loops. You always want to stop the loop when there's no new position...and I thought that NULL would be a good return result for the position to stop that loop.

But with multi-returns, if you want the overall result to be the position, you can ask for it by "circling" it:

>> [value pos]: evaluate [1 + 2 10 + 20]  ; default returns first value
== 3

>> pos
== [10 + 20]

>> [value @pos]: evaluate [1 + 2 10 + 20]  ; circle position to return it
== [10 + 20]

>> value
== 3

This means the enumeration can be separate from the return result. And it really should be that the value is the primary return result, because the primary result is the only one that can be truly invisible.

>> [^value @pos]: evaluate [comment "Hi" 10 + 20]
== [10 + 20]

>> value  ; meta ~void~ is *true* invisibility
== ~void~

Bigger Issues Loom With Things Like LET

I've mentioned the bugaboo of evaluation...the question of whether a block alone can represent the entirety of accrued state in the evaluator.

Pivotal Design Question: Is Evaluator State Just a Block

That's a huge philosophical question that the historical design of DO/NEXT assumed. If state is a block is the rule, then it means the LET construct as I've begun to imagine it should not be legal. That's very difficult to reconcile with how useful LET is.

In any case, I just wanted to update this post with some modern observations...the old problems aren't really problems anymore, so there are new worries. :slight_smile:

2 Likes