Each versus for-each and for-all

In 8th, they have the *:each as a higher order function.

So, depending on whether it's being applied to a map, array, buffer, string it works like this:

Buffer lambda b:each

So, here the buffer is iterated over and the index and value are placed on the stack for lambda to consume.

For-each neesd to create words, and for-all needs to reference the series by name which creates more visual clutter.

In VID lambdas had access to a similar functionality where value was available.

We could do the same:

Each series [ code ] 

Where code has access to the words index and value of the series being traversed.

1 Like

This old post raises a good point about wanting access to both the position and value easily from the same loop.

The concept of assuming the names (value, index) is something definitely needed in Rebmu. I'm a bit reticent to put it in a "core" function. But we should make it trivially easy to make such operators if you want it.

I had a random brainstorm today about how EACH might be a generator.

>> g: each x [1 2] [print ["running" x], x + 10]

>> g
running 1
== 11

>> g
running 2
== 12

>> g
null

A thought I had was we might then imagine FOR as something that calls a function until it returns NULL, and returns the last non-NULL thing.

>> for each x [1 2] [print ["running" x], x + 10]
running 1
running 2
== 12

Then MAP could call the function until it returns NULL, but return the results:

>> map each x [1 2] [print ["running" x], x + 10]
running 1
running 2
== [11 12]

This thought is definitely more "minecraft of programming"...breaking FOR and MAP and EACH down into generic bricks.

Though there are a couple of sticking points with it... e.g. it doesn't generalize arbitrarily to things like remove each...and if NULL is taken to signal "done" then it can't also signal BREAK. But I'm now thinking maybe generators can return NULL if they speak a multi-return protocol...where NULL is augmented with "/BRANCHED". That's looking really desirable now.

Makes for interesting possibilities:

>> map generator [count-up x 3 [yield x], count-down 3 [yield x]]
== [1 2 3 3 2 1]

FOR would be similar to UNTIL .NULL?...but if you said until .null? [ask integer!] it would keep asking for integers until you canceled, then it would return NULL. But for [ask integer!] would keep asking and then give you the last input. map [ask integer!] would give you the list of integers.

2 Likes

Something else I've been thinking about (that @BlackATTR has brought up in the past) is the fact that you often want to know when you're on the first or last step of an iteration.

There could be some sensitivity in EACH to whether the code it is running has a /FIRST or /LAST refinement (which bolsters my point about first of and last of):

>> e: each x [1 2 3] func [value /first /last] [
    if first [print "first!"]
    print [value]
    if last [print "last!"]
]

>> for :e
first!
1
2
3
last!

This could be shortened with a lambda syntax:

>> for each x [1 2 3] value/first/last -> [
    if first [print "first!"]
    print [value]
    if last [print "last!"]
]

first!
1
2
3
last!

But maybe it's EACH that should be parameterized, and they should be normal arguments, so you don't get pinned to the names:

>> for each/first/last x [1 2 3] [val fst lst] -> [
    if fst [print "first!"]
    print [value]
    if lst [print "last!"]
]

first!
1
2
3
last!

There could be a lambda shorthand for that too, maybe TUPLE!

>> for each/first/last x [1 2 3] val.fst.lst -> [
    if fst [print "first!"]
    print [value]
    if lst [print "last!"]
]

first!
1
2
3
last!

Just one of the kinds of axes to explore with generalized EACH.

2 Likes

So now that the NULL isotope concept exists, it is possible to assign one of them to mean a generator is "FINISHED" (plain NULL) and another to mean "I really meant return NULL, and I may have more values to emit." When you're doing a "map-each" the intentional return of NULL is particularly important, for saying you want to add nothing to the collection but keep going.

The third state is BREAK--which is currently handled by another mechanism (THROW). There are two possibilities for who's in charge of catching the throw:

  1. iteration constructs like FOR or MAP could catch the BREAK, and return (plain) NULL
  2. generators like EACH could catch the BREAK, and then use some third signal (NULL-BREAK?) to communicate the intent to FOR or MAP as distinct from "finished" or "actually returned NULL".

It seems kind of obvious that (1) is preferable, but I'm just mentioning (2) for completeness.

So it looks like we can try this to see how it goes. Though a single-arity MAP function is not that typical. The norm is two inputs: a collection of things of category A, and a function that transforms As into Bs. Then you get a collection of B's out.

Getting the conventional arity-2 MAP out of this to use with a function becomes messy, because Rebol has historically had this "put your iteration variable's name in a certain position" situation, and if you don't need the variable (because it's implicitly a function argument) that position has to have a placeholder

 >> map each _ [1 2 3] :negate
 == [-1 -2 -3]

This could be addressed with a lambda syntax:

 >> map each [1 2 3] x -> [x + 1]
 == [2 3 4]

So then EACH is arity 2, and MAP is arity 1. But I think that this would be one of those cases of drifting away from the Rebol spirit and starting to systemize the parts too much to where you're back to "being beholden to pure functional programming syntax limits...but without any of the pure functional programming benefits."

...or we say that you use different constructs:

 >> map each x [1 2 3] [x + 1]
 == [2 3 4]

 >> map each* [1 2 3] :negate
 == [-1 -2 -3]

But I find that grating, so the idea that keeps recurring to me is to use a skippable QUOTED! parameter:

 >> map each 'x [1 2 3] [x + 1]
 == [2 3 4]

 >> map each [1 2 3] :negate
 == [-1 -2 -3]

This has the downside (upside?) that you always have to distinuish a named loop variable by quoting it. That applies to BLOCK!s of variables too...to distinguish a block of variable names from a block of data to enumerate:

 >> map each '[a b] [1 2 3 4] [reduce [a + 1, b + 2]]
 == [2 4 4 6]

(We might shorthand this as map each 'a.b, a concept being thought about for lambdas too.)

I'll send everyone back to the not-commented-on-but-very-important "Speaking with tics" regarding this contentious (at least in my mind) matter.

Perhaps when we see this all laid out where we can get rid of the hyphen in FOR-EACH and MAP-EACH, the total cost of needing the apostrophe on the variable levels out. It also can't be denied that it helps communicate that it's a "name" and not a "use", and means that if the word is incidentally a left-quoting ENFIX operator it won't be "live" and try to quote the EACH.

This doesn't necessarily have any bearing on type of foo vs. 'type of foo. While the issues are related, different decisions can be made on each case according to what is best suited.

Broader Impliciations

If something like this is pursued, the question comes up as to how many constructs should use this "generator" style.

loop 10 [print "Hi"] is pretty straightforward. You wouldn't want LOOP to generate a function, and then have to write for loop 10 [print "Hi"].

So this needs to be weighed in consideration--though maybe it suggests things like LOOP do use generators, but are macros which subsume the FOR into their implementation.

2 Likes

While rethinking the nature of arity-1 WHILE and shifting the arity-2 WHILE semantics to LOOP, it made me think that maybe this needs reordering.

>> for x each [1 2] [print ["running" x, x + 10]]
running 1
running 2
== 12

This would mean EACH [1 2] would be the generator, and X would be a property of FOR. That gives flexibility in defining FOR for other argument values. Integer could just count up:

>> for x 2 [
      print ["running" x], x + 10
   ]
running 1
running 2
== 12

FOR X EACH and FOR-EACH X are the same number of characters, with the former having nicer visual possibilities. This is especially true when differentiating a block for variable names from the iterated variables:

for-each [a b] [1 2 3 4] [print [a b]]

for [a b] each [1 2 3 4] [print [a b]]

This makes EACH as a generator much clearer:

 >> g: each [1 2]

 >> g
 == 1

 >> g
 == 2

 >> g
 ; null

Under this setup, a FOR which did not use an EACH could reserve BLOCK! for other meanings, such as a dialect for ranges.

for x [1 to 10] [print [x]]
for x [1 thru 10] [print [x]]

But since that puts a lot of "logic" into FOR, we might make EACH the default semantics of a series with FOR and then make this dialect the weird case. Otherwise you're kind of wasting the short word on cases like strings. But it's hard to think of a good short name for that generator dialect:

for x yournamehere [1 to 10] [print [x]]
for x yournamehere [1 thru 10] [print [x]]

Anyway...if FOR was generalized in this way, it could free up REPEAT for something that might make more sense: namely "do the same thing N times". But this could be distinguished from loop by paying attention to the truthiness of the body:

>> n: 0
>> repeat 10 [  ; a WHILE variant, limited by a count.
      print "Repeating"
      n < 3
   ]
Repeating
Repeating
Repeating
Repeating

This combination of "heeds the count, but stops earlier if the body returns false" may seem odd. But this formulation could make repeating a rule N times in PARSE more clear when it's hidden behind a variable.

2 Likes