Complex loops with multiple WHILEs can be difficult to understand particularly when returning to a program written some time ago. I wanted
a more readable control statement that expressed 'what' it did instead
of 'how' it did it. Also I wanted the ability to carry out a pipeline of operations on a collection of data objects (array, linked list etc).
After some experimentation I ended up with this statement:
FOR-EACH <iterator> DO[ <pipeline operations> ]NEXT
The <iterator> is completely under the programmers control to provide
the next item in any type of data structure
Other requirements on pipeline operations:
- to be able to return to the <iterator> early i.e 'continue'
- to be able to exit the loop early i.e. 'break'
- (optional) possibly normal forth definitions
\ --------------------------------------
synonym for-each begin
synonym do[ while
synonym ]next repeat
: => ( f -- )
0 cs-pick postpone ?dup postpone 0= postpone until
; immediate
: pp=> ( -- ) postpone => ;
: p: : postpone [: ;
: ;p
postpone ;] postpone compile, postpone pp=> postpone ; immediate
; immediate
\ --------------------------------------
As can be seen this is just renaming BEGIN ... WHILE ... REPEAT for readability with some rules that can be ignored or varied as needed by
the programmer.
- Pipeline operations can be defined using P: ... ;P or : ... ; the difference being that P: incorporates the word => at the end to insert
the 'continue' control sequence. If normal colon definitions are used
and 'continue' is needed the word => must be used between operations
The rules to be used are:
- the <iterator> must return a non-zero value to continue the loop or a
zero to exit the loop.
- that pipeline operations must return a flag with possible values:
0 carry on to the next operation
-1 loop back to the iterator for the next iteration (continue)
+1 loop back to the iterator and exit the loop (break)
If neither 'continue' or 'break' are needed the FOR-EACH statement is
just a simple BEGIN ... WHILE ... REPEAT loop. Therefore normal colon definitions can be used in the pipeline.
Using a simple/silly example display integers divisible by 6 that
Michael Gassanenko used to demonstrate his similar ... START ... EMERGE
... construct, we have:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Alternatively using a 'break' in \2?
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f1 = +/-1
if 0 exit then 1+ true;
p: /2? ( n1 n2 -- n1 n2 f ) 2dup < if 1 exit then dup 2 mod 0<> ;p
p: /3? ( n -- n f ) dup 3 mod 0<> ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 63 0 true /6? .s
\ displays 6 12 18 24 30 36 42 48 54 60
Of course imposing some rules means that some efficiency is lost
compared to WHILE loops but, as I use a desktop system, speed is never a problem for me.
--
Gerry
On Friday, November 10, 2023 at 12:45:47 PM UTC-5, Gerry Jackson wrote:
\ --------------------------------------
synonym for-each begin
synonym do[ while
synonym ]next repeat
: => ( f -- )
0 cs-pick postpone ?dup postpone 0= postpone until
; immediate
: pp=> ( -- ) postpone => ;
: p: : postpone [: ;
: ;p
postpone ;] postpone compile, postpone pp=> postpone ; immediate
; immediate
\ --------------------------------------
That's a very tidy way to do this. I was looking with lust at the
Python functions MAP REDUCE and FILTER.
The key insight for me was that they require an "initial value".
I play with a retro machine so memory is tight so I just used the
dictionary in a crude way to get a kind of dynamic array storage.
This allows the HOFs to output another array which can be
further processed. You can name an array with DATA:
or just use the result directly from the data stack.
Each array is stored as a CELL counted list of cells. SIZE converts
a counted array into an (addr,len) pair.
Here is what I came up with. (ignore the INCLUDE statements) https://github.com/bfox9900/CAMEL99-ITC/blob/master/LIB.ITC/MAPCELLS.FTH
At the moment it only works for CELL data.
But of course the cells could be pointers. I just haven't gone farther.
I just tried it on GForth .73 and I broke the rules and didn't use CELL.
:-)
Now it works.
Might give you some ideas.
That's a very tidy way to do this. I was looking with lust at the
Python functions MAP REDUCE and FILTER.
Such High Order Functions were one of the motivations for the FOR-EACH statement
The key insight for me was that they require an "initial value".
I'm not sure what you mean.
I play with a retro machine so memory is tight so I just used the dictionary in a crude way to get a kind of dynamic array storage.
This allows the HOFs to output another array which can be
further processed. You can name an array with DATA:
or just use the result directly from the data stack.
On Friday, November 10, 2023 at 9:21:34 PM UTC, Brian Fox wrote:
On Friday, November 10, 2023 at 12:45:47 PM UTC-5, Gerry Jackson wrote:
\ --------------------------------------
synonym for-each begin
synonym do[ while
synonym ]next repeat
: => ( f -- )
0 cs-pick postpone ?dup postpone 0= postpone until
; immediate
: pp=> ( -- ) postpone => ;
: p: : postpone [: ;
: ;p
postpone ;] postpone compile, postpone pp=> postpone ; immediate
; immediate
\ --------------------------------------
That's a very tidy way to do this. I was looking with lust at the
Python functions MAP REDUCE and FILTER.
Such High Order Functions were one of the motivations for the FOR-EACH >statement
The key insight for me was that they require an "initial value".
I'm not sure what you mean.
Gerry
In article <uitpt1$qsjs$1@dont-email.me>,
Gerry Jackson <do-not-use@swldwa.uk> wrote:
On Friday, November 10, 2023 at 9:21:34 PM UTC, Brian Fox wrote:
On Friday, November 10, 2023 at 12:45:47 PM UTC-5, Gerry Jackson wrote: >>>> \ --------------------------------------
synonym for-each begin
synonym do[ while
synonym ]next repeat
: => ( f -- )
0 cs-pick postpone ?dup postpone 0= postpone until
; immediate
: pp=> ( -- ) postpone => ;
: p: : postpone [: ;
: ;p
postpone ;] postpone compile, postpone pp=> postpone ; immediate
; immediate
\ --------------------------------------
That's a very tidy way to do this. I was looking with lust at the
Python functions MAP REDUCE and FILTER.
Such High Order Functions were one of the motivations for the FOR-EACH
statement
The key insight for me was that they require an "initial value".
I'm not sure what you mean.
If you want to apply + to a set you have to start with 0, OTOH
with * you must start with 1. If you want to print every word in
a wordlist you don't need it.
Gerry Jackson wrote:
That's a very tidy way to do this. I was looking with lust at the
Python functions MAP REDUCE and FILTER.
Such High Order Functions were one of the motivations for the FOR-EACH
statement
The key insight for me was that they require an "initial value".
I'm not sure what you mean.
I play with a retro machine so memory is tight so I just used the
dictionary in a crude way to get a kind of dynamic array storage.
This allows the HOFs to output another array which can be
further processed. You can name an array with DATA:
or just use the result directly from the data stack.
On machines with sufficient heap, it is worth implementing dynamic arrays.
I use an array stack and array values for this. They only contain a pointer >to the array structs in the heap. Then implementing HOFs for arrays is not >difficult (my arrays are mostly long 1-dimensional signal vectors with >thousands of elements).
Nice side effect: since strings are only character arrays, you get dynamic >strings for practically nothing.
Complex loops with multiple WHILEs can be difficult to understand >particularly when returning to a program written some time ago.
Using a simple/silly example display integers divisible by 6 that
Michael Gassanenko used to demonstrate his similar ... START ... EMERGE
... construct, we have:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Given the number of cases where I have written nothing between the ?OF
and the following ENDOF or CONTOF, maybe a ?ENDOF or ?CONTOF
(equivalent to 0= ?OF ENDOF or 0= ?OF CONTOF) may be useful.
Now it works.
Might give you some ideas.
On Friday, November 10, 2023 at 10:21:34 PM UTC+1, Brian Fox wrote:
Now it works.
Might give you some ideas.
Ideas like these?
: foreach ( 'f addr count -- )
cells bounds do
I @ over execute
loop drop ;
\ where : f ( n -- m )
: map ( 'f addr count -- )
cells bounds do
I @ over execute I !
loop drop ;
\ where : f ( st n -- st' )
: reduce ( st 'f addr count -- st' )
cells bounds do
I @ swap dup >r execute r>
loop drop ;
On 14/11/2023 18:31, Hans Bezemer wrote:
On Friday, November 10, 2023 at 10:21:34 PM UTC+1, Brian Fox wrote:
Now it works.
Might give you some ideas.
Ideas like these?
: foreach ( 'f addr count -- )
cells bounds do
I @ over execute
loop drop ;
\ where : f ( n -- m )
: map ( 'f addr count -- )
cells bounds do
I @ over execute I !
loop drop ;
\ where : f ( st n -- st' )
: reduce ( st 'f addr count -- st' )
cells bounds do
I @ swap dup >r execute r>
loop drop ;
You'd need a different version of these for other collections e.g.
linked list. I was trying for a more general solution.
Its easy to write solutions like this for individual cases, its also
easy to make mistakes. Shouldn't the loops be ended with:
1 cells +loop
--
Gerry
On 14/11/2023 18:31, Hans Bezemer wrote:
On Friday, November 10, 2023 at 10:21:34 PM UTC+1, Brian Fox wrote:
Ideas like these?
: foreach ( 'f addr count -- )
cells bounds do
I @ over execute
loop drop ;
where : f ( n -- m )
: map ( 'f addr count -- )
cells bounds do
I @ over execute I !
loop drop ;
where : f ( st n -- st' )
: reduce ( st 'f addr count -- st' )
cells bounds do
I @ swap dup >r execute r>
loop drop ;
Its easy to write solutions like this for individual cases, its also
easy to make mistakes. Shouldn't the loops be ended with:
1 cells +loop
Also I wanted the ability to carry out a pipeline of operations on a collection of data objects (array, linked list etc).
Gerry Jackson <do-not-use@swldwa.uk> writes:
Complex loops with multiple WHILEs can be difficult to understand
particularly when returning to a program written some time ago.
Of course this makes me think of the extended case construct (present
in development Gforth).
Your variant is interesting, but it's not clear enough to me what it
can do. So let's see:
Using a simple/silly example display integers divisible by 6 that
Michael Gassanenko used to demonstrate his similar ... START ... EMERGE
... construct, we have:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Let's see how this turns out with the extended CASE:
: /6? ( limit start -- )
case ( limit n )
1+ 2dup < ?of 2drop endof
dup 2 mod ?of contof
dup 3 mod ?of contof
dup .
next-case ;
cr 50 0 /6?
This produces the expected output, and you can see how the parts of
the definition relate to the code in your variant.
Maybe there is an example where FOR-EACH ... DO[ ... ]NEXT is harder
to replace.
Given the number of cases where I have written nothing between the ?OF
and the following ENDOF or CONTOF, maybe a ?ENDOF or ?CONTOF
(equivalent to 0= ?OF ENDOF or 0= ?OF CONTOF) may be useful.
On Fri, Nov 10 2023, Gerry Jackson wrote:
Also I wanted the ability to carry out a pipeline of operations on a collection of data objects (array, linked list etc).
I wanted to have something like Rust's iterator library and played
around with the code below. With this, the divisible-by-6 example could
be written as:
: /2? ( n -- flag ) 2 mod 0= ;
: /3? ( n -- flag ) 3 mod 0= ;
: /6? ( start end -- )
[range-iterator]
[ ' /2? ] [filter]
[ ' /3? ] [filter]
[ ' . ] [for-each]
;
This is arguably very close to the Rust expression:
(start..end)
.filter(|u| u % 2 == 0)
.filter(|u| u % 3 == 0)
.for_each(|u| print!("{u} "));
Unfortunately, my code is much more complicated that yours. On the plus side, I'm trying to mimic Rust's operators and naming, which saves me
from re-inventing wheels and perhaps helps to communicate the intention behind the operators.
Helmut
Couldn't ?OF parse the next word to see if it is ?ENDOF or ?CONTOF and generate the appropriate code if so?
On 14/11/2023 17:30, Anton Ertl wrote:
Gerry Jackson <do-not-use@swldwa.uk> writes:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Let's see how this turns out with the extended CASE:
: /6? ( limit start -- )
case ( limit n )
1+ 2dup < ?of 2drop endof
dup 2 mod ?of contof
dup 3 mod ?of contof
dup .
next-case ;
cr 50 0 /6?
This produces the expected output, and you can see how the parts of
the definition relate to the code in your variant.
Your extended case is very versatile so I'm not surprised it can do the
same function. However you are missing the point of what I'm trying to >achieve. This is to have a versatile looping structure that is more
readable, one that hides the nuts and bolts of the code that implements
the loop.
Given the number of cases where I have written nothing between the ?OF
and the following ENDOF or CONTOF, maybe a ?ENDOF or ?CONTOF
(equivalent to 0= ?OF ENDOF or 0= ?OF CONTOF) may be useful.
Couldn't ?OF parse the next word to see if it is ?ENDOF or ?CONTOF and >generate the appropriate code if so?
The idea behind ?ENDOF and ?CONTOF is to increase readability by
making the code shorter, not optimization. So it might be:
: /6? ( limit start -- )
case
1+ 2dup < ?endof
dup 2 mod ?contof dup 3 mod ?contof dup . next-case
2drop ;
As for optimizing ?OF CONTOF, I would not do it with a parsing word,
because of bad experiences with parsing words. I would probably do it
by optimizing a conditional branch (?OF) that just jumps over an >unconditional branch (ENDOF): just invert the sense of the conditional
branch and let it's target be that of the unconditional branch; leave
the unconditional branch away.
- anton
Its easy to write solutions like this for individual cases, its alsoTrue. But this code was before 4tH could optimize 1 CELLS +LOOP away. Since
easy to make mistakes. Shouldn't the loops be ended with:
1 cells +loop
Gerry Jackson <do-not-use@swldwa.uk> writes:
On 14/11/2023 17:30, Anton Ertl wrote:
Gerry Jackson <do-not-use@swldwa.uk> writes:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Let's see how this turns out with the extended CASE:
: /6? ( limit start -- )
case ( limit n )
1+ 2dup < ?of 2drop endof
dup 2 mod ?of contof
dup 3 mod ?of contof
dup .
next-case ;
cr 50 0 /6?
This produces the expected output, and you can see how the parts of
the definition relate to the code in your variant.
Your extended case is very versatile so I'm not surprised it can do the
same function. However you are missing the point of what I'm trying to
achieve. This is to have a versatile looping structure that is more
readable, one that hides the nuts and bolts of the code that implements
the loop.
The reader needs to be familiar with FOR-EACH ... DO[ ... ]NEXT and
P: ... ;P to make this more readable. The question is if this will be
used frequently enough to make readers familiar with these words.
People have already argued against the extended CASE because of unfamiliarity.
OTOH, if there are enough uses of the extended CASE for the
filter-loop pattern, one might also find it readable (and recognize
that it is a filter loop) when expressed using the extended CASE.
Using that the implemention is straightforward.
: for-each ( RT: -- ) postpone case ; immediate
: do[ ( RT: f -- ) postpone ?break ; immediate
: ]next ( RT: x -- ) postpone dup postpone next-case ; immediate
: ?next ( RT: f -- ) postpone ?of postpone contof ; immediate
?BREAK is defined in the post at the above link and is equivalent to:
?OF ENDOF
if we want to us a similar syntax to Helmut Eller's excellent post, we
could define:
synonym filter ?next
The example used previously is then:
: n+1 ( n1 n2 -- n1 n2+1 f ) \ f true when n1<n2
1+ 2dup <
;
: /2? ( n -- n f ) dup 2 mod ;
: /3? ( n -- n f ) dup 3 mod ;
: .n ( n -- n ) dup . ;
: /6? ( n1 n2 -- )
for-each n+1 do[ /2? ?next /3? ?next .n ]next 2drop
;
50 0 /6? .s
Incidentally I gave up having P: ... ;P as a bad idea.
Gerry Jackson <do-not-use@swldwa.uk> writes:
Complex loops with multiple WHILEs can be difficult to understand
particularly when returning to a program written some time ago.
Of course this makes me think of the extended case construct (present
in development Gforth).
Your variant is interesting, but it's not clear enough to me what it
can do. So let's see:
Using a simple/silly example display integers divisible by 6 that
Michael Gassanenko used to demonstrate his similar ... START ... EMERGE
... construct, we have:
: n+1 ( n1 n2 f1 -- n1 n2+1 f2 ) \ f2 true when n1<n2
drop 1+ 2dup >=
;
p: /2? ( n -- n f ) dup 2 mod ;p
p: /3? ( n -- n f ) dup 3 mod ;p
: .n ( n -- n ) dup . -1 ;
: /6? ( n1 n2 -- )
0 for-each n+1 do[ /2? /3? .n ]next 2drop
;
cr 50 0 /6? .s
\ displays 6 12 18 24 30 36 42 48
Let's see how this turns out with the extended CASE:
: /6? ( limit start -- )
case ( limit n )
1+ 2dup < ?of 2drop endof
dup 2 mod ?of contof
dup 3 mod ?of contof
dup .
next-case ;
cr 50 0 /6?
... the word "/6?" can be defined as follows:
: /6? ( limit start -- )
repeat{ ( limit n )
1+ 2dup < if-break{ 2drop }
dup 2 mod if-cont{}
dup 3 mod if-cont{}
dup .
}repeat
;
On 21/11/2023 11:02 am, Ruvim wrote:
... the word "/6?" can be defined as follows:
: /6? ( limit start -- )
repeat{ ( limit n )
1+ 2dup < if-break{ 2drop }
dup 2 mod if-cont{}
dup 3 mod if-cont{}
dup .
}repeat
;
: ?six ( n -- n )
dup 2 mod if exit then
dup 3 mod if exit then
dup .
;
: /6? ( limit start -- )
begin
1+ 2dup < 0=
while
?six
repeat 2drop
;
On 2023-11-21 05:22, dxf wrote:
On 21/11/2023 11:02 am, Ruvim wrote:
... the word "/6?" can be defined as follows:
: /6? ( limit start -- )
repeat{ ( limit n )
1+ 2dup < if-break{ 2drop }
dup 2 mod if-cont{}
dup 3 mod if-cont{}
dup .
}repeat
;
: ?six ( n -- n )
dup 2 mod if exit then
dup 3 mod if exit then
dup .
;
: /6? ( limit start -- )
begin
1+ 2dup < 0=
while
?six
repeat 2drop
;
Your variant is longer by 3 lines out of 8, which is 3/8 = 38%
Another rationale is the same as for quotations: sometime you don't want to introduce a new word that is used only once.
Your variant is longer by 3 lines out of 8, which is 3/8 = 38%
On Sun, Nov 19 2023, Gerry Jackson wrote:
Using that the implemention is straightforward.
: for-each ( RT: -- ) postpone case ; immediate
: do[ ( RT: f -- ) postpone ?break ; immediate
: ]next ( RT: x -- ) postpone dup postpone next-case ; immediate
I think the DUP is too much as NEXT-CASE, unlike ENDCASE, doesn't drop. Either way, the stack comment doesn't seem to match the code.
: ?next ( RT: f -- ) postpone ?of postpone contof ; immediate
?BREAK is defined in the post at the above link and is equivalent to:
?OF ENDOF
I.e.:
: ?break ( RT: f -- ) postpone ?of postpone endof ; immediate
if we want to us a similar syntax to Helmut Eller's excellent post, we
could define:
synonym filter ?next
It would probably be inverted:
: filter ( RT: f -- ) postpone 0= postpone ?next ; immediate
The example used previously is then:
: n+1 ( n1 n2 -- n1 n2+1 f ) \ f true when n1<n2
1+ 2dup <
;
: /2? ( n -- n f ) dup 2 mod ;
: /3? ( n -- n f ) dup 3 mod ;
: .n ( n -- n ) dup . ;
: /6? ( n1 n2 -- )
for-each n+1 do[ /2? ?next /3? ?next .n ]next 2drop
;
50 0 /6? .s
Mathematically speaking, zero is divisible by 6. So I think 0 should be printed too. This could probably done with a different definition for
n+1.
Also the naming is a bit curious: isn't /2? supposed to read as
"is divisible by 2"? But the implementation returns the inverse: true if
it is not divisible.
Incidentally I gave up having P: ... ;P as a bad idea.
It think there was something useful to the idea that a helper can return three values: -1, 0, +1. In particular it could be useful to separate
the "break because we're finished" case from the "break because we've encountered an error" situation.
Helmut
The revised definitions become:
: for-each ( RT: -- ) postpone case ; immediate \ RT: is run time
: do[ ( RT: f -- ) postpone ?break ; immediate
: ]next ( RT: x -- ) postpone next-case ; immediate
: ?next ( RT: f -- ) postpone ?of postpone contof ; immediate
\ Example
: n+1 ( n1 n2 -- n1 n2+1 f ) \ f true when n1<n2
1+ 2dup <
;
: mod2? ( n -- n f ) dup 2 mod ;
: mod3? ( n -- n f ) dup 3 mod ;
: .n ( n -- n ) dup . ;
: /6? ( n1 n2 -- )
1- for-each n+1 do[ mod2? ?next mod3? ?next .n ]next 2drop
;
50 0 /6? .s \ displays 0 6 12 18 24 30 36 42 48
Thanks for your comments.
IIRC cs-drop had been a 200x proposal but was ultimately shelved.
: for-each...
postpone ahead postpone begin postpone 0< postpone if
postpone begin 3 cs-roll postpone then
; immediate
Example B - MAP with locals for the xts...
: map ( ... iter-xt op-xt -- ... )
{: it-xt op-xt :} for-each it-xt execute do[ op-xt execute ]next
;
d:/projects/forthapps/experimental/src/for-each-while.fth:178: error: >Undefined word
{: it-xt op-xt :} for-each >>>it-xt<<< execute do[ op-xt execute ]next
Backtrace:
0 $6FFFF7FF1B8 throw
*** GForth error reported ***
i.e. it didn't recognise the local it-xt
On 2023-12-07 00:46, Gerry Jackson wrote:
On 28/11/2023 11:50, Gerry Jackson wrote:
The revised definitions become:
: for-each ( RT: -- ) postpone case ; immediate \ RT: is run time
: do[ ( RT: f -- ) postpone ?break ; immediate
: ]next ( RT: x -- ) postpone next-case ; immediate
: ?next ( RT: f -- ) postpone ?of postpone contof ; immediate
\ Example
: n+1 ( n1 n2 -- n1 n2+1 f ) \ f true when n1<n2
1+ 2dup <
;
: mod2? ( n -- n f ) dup 2 mod ;
: mod3? ( n -- n f ) dup 3 mod ;
: .n ( n -- n ) dup . ;
: /6? ( n1 n2 -- )
1- for-each n+1 do[ mod2? ?next mod3? ?next .n ]next 2drop
;
50 0 /6? .s \ displays 0 6 12 18 24 30 36 42 48
Thanks for your comments.
I've been refining the implementation of the FOR-EACH statement and
have re-implented it based on the BEGIN ... WHILE ... REPEAT loop
instead of the GForth extended CASE. Differences from the previous
version are:
- the iterator can ignore whether the pipeline operations CONTINUE or
BREAK as they jump back to a conditional jump just before the iterator
- the ]NEXT code jumps back to the iterator itself
- the pipeline operator has changed from => to |> which is used in
other languages
These make the control flow rather convoluted.
The FOR-EACH definitions are:
: cs-drop-dest ( CS: dest -- )
postpone ahead 1 cs-roll postpone again postpone then
;
: for-each
postpone ahead postpone begin postpone 0< postpone if
postpone begin 3 cs-roll postpone then
; immediate
synonym do[ while [defined] [SwiftForth] [if] immediate [then]
: ]next
postpone repeat postpone then cs-drop-dest
; immediate
\ The pipeline operator
: |> ( f -- ) 3 cs-pick postpone ?dup postpone 0= postpone until ;
immediate
This ran the previous examples on 6 different Forth systems correctly
but with the following examples 2 of the 6 Forths failed.
Example A - MAP without locals
: map ( ... iter-xt op-xt -- ... )
2>r for-each 2r@ drop execute do[ r@ execute ]next 2r> 2drop
;
Example B - MAP with locals for the xts
: map ( ... iter-xt op-xt -- ... )
{: it-xt op-xt :} for-each it-xt execute do[ op-xt execute ]next
;
Testing these alternatives with the following data and definitions
that simply square the items in an array:
\ ------------------------------------
create a[] 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ,
10 constant a-len
:noname ( ad1 ad2 -- ad1 ad2' f ) \ ad1 is last array item
cell+ 2dup u>
; constant next-item
:noname ( ad -- ad ) dup @ dup * over ! ; constant squared
: init ( ad n -- ad+n' ad ) cells over + swap 1 cells - ;
: square[] ( ad n -- ) init next-item squared map 2drop ;
a[] a-len square[]
: .a[] init next-item [: ( ad -- ad ) dup @ . ;] map 2drop ;
a[] a-len .a[] .s
\ ---------------------------------
The first system to fail is GForth which gave the correct display for
example A
1 4 9 16 25 36 49 64 81 100
But failed to compile example B giving:
d:/projects/forthapps/experimental/src/for-each-while.fth:178: error:
Undefined word
{: it-xt op-xt :} for-each >>>it-xt<<< execute do[ op-xt execute
]next
Backtrace:
0 $6FFFF7FF1B8 throw
*** GForth error reported ***
i.e. it didn't recognise the local it-xt
This may be unfair to GForth as I am using version 0.7.9 20180905 as
that was the last Windows executable in the available GForth
snapshots. As I'm not set up to compile the latest GForth snapshots
I'd be grateful if someone could try running the above on a recent
version.
I see the same issue in Gforth 0.7.9_20230921
It fails to compile the following test case:
: foo {: x :} ahead begin x exit again then ;
But it compiles the cases:
: foo {: x :} ahead x exit then ;
: foo {: x :} begin x exit again ;
: foo {: x :} begin ahead x exit then again ;
\ --------------------------------
The other system to fail was VFX Forth version 5.43 build 4238 which,
I believe, is the latest version available for download.
VFX Forth ran example B correctly but with Example A it failed with:
Err# -22 ERR: Control structure mismatch.
Source: "src/for-each-while.fth" on line 163
-> : square[] ( ad n -- ) init next-item squared map 2drop ;
^
\ -------------------------------
The four systems that worked were SwiftForth, MinForth, Alex
McDonald's wf32 and my system. Swiftforth did need DO[ to be declared
IMMEDIATE
Both examples also work in minForth/3.4.8 and SP-Forth/4.29
--
Ruvim
On 2023-12-07 00:46, Gerry Jackson wrote:
On 28/11/2023 11:50, Gerry Jackson wrote:
The revised definitions become:
: for-each ( RT: -- ) postpone case ; immediate \ RT: is run time
: do[ ( RT: f -- ) postpone ?break ; immediate
: ]next ( RT: x -- ) postpone next-case ; immediate
: ?next ( RT: f -- ) postpone ?of postpone contof ; immediate
\ Example
: n+1 ( n1 n2 -- n1 n2+1 f ) \ f true when n1<n2
1+ 2dup <
;
: mod2? ( n -- n f ) dup 2 mod ;
: mod3? ( n -- n f ) dup 3 mod ;
: .n ( n -- n ) dup . ;
: /6? ( n1 n2 -- )
1- for-each n+1 do[ mod2? ?next mod3? ?next .n ]next 2drop
;
50 0 /6? .s \ displays 0 6 12 18 24 30 36 42 48
Thanks for your comments.
I've been refining the implementation of the FOR-EACH statement and
have re-implented it based on the BEGIN ... WHILE ... REPEAT loop
instead of the GForth extended CASE. Differences from the previous
version are:
- the iterator can ignore whether the pipeline operations CONTINUE or
BREAK as they jump back to a conditional jump just before the iterator
- the ]NEXT code jumps back to the iterator itself
- the pipeline operator has changed from => to |> which is used in
other languages
These make the control flow rather convoluted.
The FOR-EACH definitions are:
: cs-drop-dest ( CS: dest -- )
postpone ahead 1 cs-roll postpone again postpone then
;
A simpler variant:
: cs-drop-dest ( CS: dest -- )
postpone true postpone until
;
: for-each
postpone ahead postpone begin postpone 0< postpone if
postpone begin 3 cs-roll postpone then
; immediate
synonym do[ while [defined] [SwiftForth] [if] immediate [then]
: ]next
postpone repeat postpone then cs-drop-dest
; immediate
\ The pipeline operator
: |> ( f -- ) 3 cs-pick postpone ?dup postpone 0= postpone until ;
immediate
Actually, the pipeline operator has the stack diagram ( n -- )
if n = 0 -- continue the pipeline
if n < 0 -- break the pipeline, continue the loop
if n > 0 -- break both the pipeline and the loop
( n -- ) if n = 0 -- continue the pipelinefirst BEGIN in FOR_EACH where:
( n -- n ) otherwise -- breaks the pipeline and branches back to the
Explanation for other readers.
The code fragment:
for-each
...
do[
...
|>
...
]next
Is compiled as follows:
ahead begin 0< if begin [ 3 cs-roll ] then
...
while
...
[ 3 cs-pick ] ?dup 0= until
...
repeat then
ahead [ 1 cs-roll ] again then
Using the same indentation for the same control flow structure instance,
it can be written as follows:
ahead
begin
0<
if
begin
[ 3 cs-roll ]
then
...
while
...
?dup 0=
[ 3 cs-pick ]
until
...
repeat
then
ahead
[ 1 cs-roll ]
again \ this line is unreachable in run-time
then
NB: "cs-roll" and "cs-pick" allows us to intersect the different
instances of control-flow structures.
--
Ruvim
Gerry Jackson <do-not-use@swldwa.uk> writes:
: for-each...
postpone ahead postpone begin postpone 0< postpone if
postpone begin 3 cs-roll postpone then
; immediate
Example B - MAP with locals for the xts...
: map ( ... iter-xt op-xt -- ... )
{: it-xt op-xt :} for-each it-xt execute do[ op-xt execute ]next
;
d:/projects/forthapps/experimental/src/for-each-while.fth:178: error:
Undefined word
{: it-xt op-xt :} for-each >>>it-xt<<< execute do[ op-xt execute ]next >> Backtrace:
0 $6FFFF7FF1B8 throw
*** GForth error reported ***
i.e. it didn't recognise the local it-xt
This is due to a weakness in automatic scoping of locals in Gforth
(since 1994). You can read all about it in <https://gforth.org/manual/Where-are-locals-visible-by-name_003f.html>.
You can work around it by changing FOR-EACH as follows:
[undefined] assume-live [if] : assume-live ; immediate [then]
: for-each
postpone ahead postpone assume-live postpone begin postpone 0< postpone if
postpone begin 3 cs-roll postpone then
; immediate
- anton
On 07/12/2023 06:54, Anton Ertl wrote:...
This is due to a weakness in automatic scoping of locals in Gforth
(since 1994). You can read all about it in
<https://gforth.org/manual/Where-are-locals-visible-by-name_003f.html>.
Thanks for pointing me to that, but is that the correct approach to take
when doing something non-standard?
What I mean is that you've implemented a nice computer science concept
in GForth which can be broken by use of a low level facility (CS-PICK
CS-ROLL AHEAD etc) provided by standard Forth. Then someone using that >facility is surprised when some standard Forth code fails.
Wouldn't a better approach be to implement the standard correctly but
offer the improved locals visibility approach as an option.
Why non-standard?
13.3.3.2 Syntax restrictions
b) The position in program source at which the sequence of
(LOCAL) messages is sent, referred to here as the point at
which locals are declared, shall not lie within the scope
of any control structure;
Gerry Jackson <do-not-use@swldwa.uk> writes:
On 07/12/2023 06:54, Anton Ertl wrote:...
This is due to a weakness in automatic scoping of locals in Gforth
(since 1994). You can read all about it in
<https://gforth.org/manual/Where-are-locals-visible-by-name_003f.html>.
Thanks for pointing me to that, but is that the correct approach to take
when doing something non-standard?
When a program does something non-standard, it's up to the system how
to react, but in this case this aspect of the program is standard, and
Gforth does not work as required by the standard.
What I mean is that you've implemented a nice computer science concept
in GForth which can be broken by use of a low level facility (CS-PICK
CS-ROLL AHEAD etc) provided by standard Forth. Then someone using that
facility is surprised when some standard Forth code fails.
Wouldn't a better approach be to implement the standard correctly but
offer the improved locals visibility approach as an option.
I think the right solution is to assume the following visibility on
AHEAD BEGIN: the locals visible at the most recent place where nothing
was on the control-flow stack. So the locals defined according to the standard are visible in the whole colon definition after the
definition, as the standard requires.
This should not make anything visible that must not be visible
according to the principles behind automatic scoping: If a local is
visible at a point without control-flow (or SCOPE), everything
afterwards in the definition is only reachable through that point (or
it is unreachable, in which case the visibility is irrelevant), so the
locals visible at that point are also visible there.
This can actually be relaxed a little more: use locals visible at the
last place P where only dests are on the control-flow stack (and the
LEAVE stack is empty). Dests correspond to backwards edges in the control-flow graph, so everything afterwards is only reachable through
P.
- anton
As Gforth can declare locals within control structures it is clearly non-standard as the restriction you quote uses 'shall not' instead of
making it an ambiguous condition. I don't see why that restriction is
there (apart from within DO loops which traditionally use the return
stack) as declaring locals like that can be useful, for example in WHILE loops with the pattern:
(ad1 ad2) begin {: a b :} ... while ... a char+ b char+ repeat ...
where the updated locals get auto loaded without needing TO
Gerry Jackson wrote:
As Gforth can declare locals within control structures it is clearly
non-standard as the restriction you quote uses 'shall not' instead of
making it an ambiguous condition. I don't see why that restriction is
there (apart from within DO loops which traditionally use the return
stack) as declaring locals like that can be useful, for example in
WHILE loops with the pattern:
(ad1 ad2) begin {: a b :} ... while ... a char+ b char+ repeat ...
where the updated locals get auto loaded without needing TO
The standard distinguishes between <arg> and <val> locals. The latter
are not initialised; in the locals declaration they appear after a
vertical bar character.
IIRC in gforth you can use {: ... :} multiple times within a word
definition. It would therefore be perfectly possible to use {: | b :}
within a control structure without logical conflicts.
Personally, I find the whole thing rather less helpful.
Gerry Jackson wrote:<SNIP>
IIRC in gforth you can use {: ... :} multiple times within a word
definition. It would therefore be perfectly possible to use {: | b :}
within a control structure without logical conflicts.
On 08/12/2023 01:22, minforth wrote:
The standard distinguishes between <arg> and <val> locals. The latter
are not initialised; in the locals declaration they appear after a
vertical bar character.
IIRC in gforth you can use {: ... :} multiple times within a word
definition. It would therefore be perfectly possible to use {: | b :}
within a control structure without logical conflicts.
Personally, I find the whole thing rather less helpful.
I'm not sure what you mean by 'the whole thing'. Is it the locals in
general, Gforth extensions to locals or the FOR-EACH control structure?
I'm curious about why you think auto-scoping locals is worth the trouble
when the philosophy of Forth in general is 'let the programmer
beware'.
What I was trying to say was that Gforth requiring a workaround to
compile a standard section of code is not user friendly irrespective of >whether Gforth is non-standard.
As Gforth can declare locals within control structures it is clearly >non-standard as the restriction you quote uses 'shall not' instead of
making it an ambiguous condition.
I don't see why that restriction is
there
(apart from within DO loops which traditionally use the return
stack)
The standard distinguishes between <arg> and <val> locals. The latter
are not initialised; in the locals declaration they appear after a
vertical bar character.
IIRC in gforth you can use {: ... :} multiple times within a word
definition. It would therefore be perfectly possible to use {: | b :}
within a control structure without logical conflicts.
On 2023-12-08 12:11, Gerry Jackson wrote:
[...]
Two things about FOR-EACH I don't think I've yet mentioned.
The pipeline operator is optional. Without it the whole thing reverts
to a BEGIN ... WHILE ... REPEAT loop with a small amount of redundant
code. So why use FOR-EACH, IMHO the FOR-EACH is arguably more readable.
The pipeline operator can make the FOR-EACH statement equivalent to
multi-WHILE loops without the drawbacks of ELSE parts being separated
in the source code.
OTOH, FOR-EACH part is also optional. A pipeline operator can be used instead:
FOR-EACH TRUE DO[ next-item |> check-item |> process-item ]NEXT
It's not obvious to me that placing "next-item" into a separate section rather than into the chain (like in this example) makes readability better.
Also, I would use different operators to break and to continue the loop, instead of different signs of the input value.
In addition using IF statements before the |> operator the programmer
has the option of continuing the loop, breaking the pipeline and
continuing or breaking the loop. This is also true of Gforth's CASE
extensions but I would argue readability again.
A small clarification: the operator "|>" cannot be placed inside an "IF" statement. Only its input value can be calculated (using "IF", or any
other words).
--
Ruvim
Gerry Jackson <do-not-use@swldwa.uk> writes:
I'm curious about why you think auto-scoping locals is worth the trouble
when the philosophy of Forth in general is 'let the programmer
beware'.
I don't care that much for claims about "the philosophy of Forth". I
have seen too much nonsense advocated with this claimed philosophy;
things along the lines of: The philosophy demands that we do it this
way, because it makes this particular word shorter by a few bytes; who
cares if this word is used dozens of times, and every use becomes
harder, more error-prone, or even bigger or slower. I care!
Anyway, you have given an example of why it is useful to define locals
inside control structures in <ukteh9$1dfna$1@dont-email.me>, and I use
that a lot. Now, if we want that, but are also required by the
standard to implement general control flow as discussed in A.3.2.3.2,
I need to define the lifetime and visibility of the locals, and
ideally they should live at least as long as they are visible. I
found a solution to this problem [ertl94l], so I implemented it.
Now, 29 years later, there is the first complaint about the main
weakness of the solution, but fortunately it is possible to fix that,
too.
@InProceedings{ertl94l,
author = "M. Anton Ertl",
title = "Automatic Scoping of Local Variables",
booktitle = "EuroForth~'94 Conference Proceedings",
year = "1994",
address = "Winchester, UK",
pages = "31--37",
url = "http://www.complang.tuwien.ac.at/papers/ertl94l.ps.gz",
abstract = "In the process of lifting the restrictions on using
locals in Forth, an interesting problem poses
itself: What does it mean if a local is defined in a
control structure? Where is the local visible? Since
the user can create every possible control structure
in ANS Forth, the answer is not as simple as it may
seem. Ideally, the local is visible at a place if
the control flow {\em must} pass through the
definition of the local to reach this place. This
paper discusses locals in general, the visibility
problem, its solution, the consequences and the
implementation as well as related programming style
questions."
}
On 2023-12-09 00:49, Gerry Jackson wrote:
On 08/12/2023 10:47, Ruvim wrote:
On 2023-12-08 12:11, Gerry Jackson wrote:
[...]
Two things about FOR-EACH I don't think I've yet mentioned.
The pipeline operator is optional. Without it the whole thing reverts
to a BEGIN ... WHILE ... REPEAT loop with a small amount of redundant
code. So why use FOR-EACH, IMHO the FOR-EACH is arguably more readable. >>>>
The pipeline operator can make the FOR-EACH statement equivalent to
multi-WHILE loops without the drawbacks of ELSE parts being separated
in the source code.
OTOH, FOR-EACH part is also optional. A pipeline operator can be used
instead:
FOR-EACH TRUE DO[ next-item |> check-item |> process-item ]NEXT
It's not obvious to me that placing "next-item" into a separate
section rather than into the chain (like in this example) makes
readability better.
You're probably right, my intention was to highlight that the first item
had to be something that iterated through a collection. But without
thinking it through I guess that's a fairly minor change to make. It
would also eliminate the need for DO[
Also, I would use different operators to break and to continue the
loop, instead of different signs of the input value.
Do you mean three operators that could be placed between the pipeline
operations i.e. retaining the
iterator pipe-operator operation pipe-operator ... sequence
where we have three different pipe operators that, for the sake of
argument, might be:
|> for Proceed currently handled by a value 0
<|x for exit pipeline back, currently <0, like C continue
|x> for exit pipeline forward, currently >0, like C break
But without thinking about it deeply I don't see how that would work as
the decision about which option to take is at run-time not compile-time.
Whether this is managed by conditional jumps or possibly quotations ISTM
that there is always a choice of 1 from 3 has to be made at runtime.
Or did you have another idea in mind?
I mean a compile-time decision. My arguments are as follows.
1. In most use cases, you know at compile-time whether you need to
continue or break the loop (I mean, in the case of a pipeline break).
Then, it's better for readability to use different pipe-operators for
these cases.
2. For code reuse, it's better if an operation returns (and a
pipe-operator accepts) a value of one of two disjoint types {0, x/0},
rather than one of three disjoint types {0, +n/0, -n/0} (where 0 is a >singleton {"0"}, and "\" means a set difference).
If a pipe-operator distinguishes two types of the input value, two
operators are enough. And if we use zero to break a pipeline, these
operators can be defined via your "|>" as follows:
: |>| ( 0|x -- ) postpone 0= postpone |> ; immediate
: |<| ( 0|x -- ) postpone 0= postpone abs postpone |> ; immediate
----
Ruvim
On 2023-12-09 00:49, Gerry Jackson wrote:
On 08/12/2023 10:47, Ruvim wrote:
On 2023-12-08 12:11, Gerry Jackson wrote:
[...]
Two things about FOR-EACH I don't think I've yet mentioned.
The pipeline operator is optional. Without it the whole thing
reverts to a BEGIN ... WHILE ... REPEAT loop with a small amount of
redundant code. So why use FOR-EACH, IMHO the FOR-EACH is arguably
more readable.
The pipeline operator can make the FOR-EACH statement equivalent to
multi-WHILE loops without the drawbacks of ELSE parts being
separated in the source code.
OTOH, FOR-EACH part is also optional. A pipeline operator can be used
instead:
FOR-EACH TRUE DO[ next-item |> check-item |> process-item ]NEXT
It's not obvious to me that placing "next-item" into a separate
section rather than into the chain (like in this example) makes
readability better.
You're probably right, my intention was to highlight that the first
item had to be something that iterated through a collection. But
without thinking it through I guess that's a fairly minor change to
make. It would also eliminate the need for DO[
Also, I would use different operators to break and to continue the
loop, instead of different signs of the input value.
Do you mean three operators that could be placed between the pipeline
operations i.e. retaining the
iterator pipe-operator operation pipe-operator ... sequence
where we have three different pipe operators that, for the sake of
argument, might be:
|> for Proceed currently handled by a value 0
<|x for exit pipeline back, currently <0, like C continue
|x> for exit pipeline forward, currently >0, like C break
But without thinking about it deeply I don't see how that would work
as the decision about which option to take is at run-time not
compile-time.
Whether this is managed by conditional jumps or possibly quotations
ISTM that there is always a choice of 1 from 3 has to be made at runtime.
Or did you have another idea in mind?
I mean a compile-time decision. My arguments are as follows.
1. In most use cases, you know at compile-time whether you need to
continue or break the loop (I mean, in the case of a pipeline break).
Then, it's better for readability to use different pipe-operators for
these cases.
2. For code reuse, it's better if an operation returns (and a
pipe-operator accepts) a value of one of two disjoint types {0, x/0},
rather than one of three disjoint types {0, +n/0, -n/0} (where 0 is a singleton {"0"}, and "\" means a set difference).
If a pipe-operator distinguishes two types of the input value, two
operators are enough. And if we use zero to break a pipeline, these
operators can be defined via your "|>" as follows:
: |>| ( 0|x -- ) postpone 0= postpone |> ; immediate
: |<| ( 0|x -- ) postpone 0= postpone abs postpone |> ; immediate
On 13-12-2023 22:47, Gerry Jackson wrote:
OK, I made a high level Forth version WITH size.
You only get the address, that's it.
It's full of 4tH-isms, but I guess one will manage.
[UNDEFINED] foreach [IF]
\ 'f ( addr --)
: foreach ( 'f addr count size -- )
tuck * >r -rot r> bounds ?do
i over execute over ( size 'f size)
+loop drop drop ( --)
;
\ 'f ( st addr -- st')
: reduce ( st 'f addr count size -- st')
tuck * >r -rot >r rot r> r> bounds ?do
over i swap execute >r over r> swap
+loop nip nip ( st')
;
aka foreach map
[THEN]
struct
field: one
field: two
end-struct /bla
6 constant #bla
#bla /bla * array bla
[: dup bla - swap -> two ! ;] bla #bla /bla foreach
[: -> two ? ;] bla #bla /bla foreach depth . cr
[: -> two dup @ dup * swap ! ;] bla #bla /bla map depth . cr
[: -> two ? ;] bla #bla /bla foreach depth . cr
0 [: -> two @ + ;] bla #bla /bla reduce . depth .
Hans Bezemer
Sysop: | Keyop |
---|---|
Location: | Huddersfield, West Yorkshire, UK |
Users: | 300 |
Nodes: | 16 (3 / 13) |
Uptime: | 43:04:27 |
Calls: | 6,709 |
Calls today: | 2 |
Files: | 12,243 |
Messages: | 5,354,017 |