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
\ iter.fth --- Rust inspired iterators
\
\ Iterators are built around a function of type:
\
\ next ( state -- state' item flag )
\
\ i.e. the NEXT function computes the next state of the iterator and
\ yields ITEMs. FLAG is true if the item is valid and false if the
\ iterator is finished (in that case the item is invalid).
\
\ This code tries to be “generic” in the sense that STATE and ITEM can
\ be more than a single stack item. E.g. to iterate over the lines in
\ a file, ITEM can be a pair C-ADDR U to represent the string. The
\ size of STATE and ITEM must be known at compile time.
\
\ An example for client code is:
\
\ : foo ( ) 0 10 [range-iterator] [ ' . ] [for-each] ;
\ foo
\
\ As in Rust, a “range” is an interval of integers, and the example
\ prints the integers from 0 up to 9.
\
\ At compile time, [RANGE-ITERATOR] puts a data structure on the stack
\ that describes, among other things, the size of STATE and NEXT.
\ [FOR-EACH] uses this structure to assemble the loop. In this
\ example the produced code looks like this:
\
\ : foo 0 10 BEGIN %range-next 0<> WHILE . REPEAT drop 2drop ;
\
\ Here, %RANGE-NEXT is a specific next function that moves the
\ iterator from one state to the next. The STATE for a range iterator
\ requires two stack slots and the ITEM one slot. [FOR-EACH] also
\ assumes that the xt (produced by the [ ' . ] part in the example)
\ has type ( ITEM -- ).
\
forth-wordlist wordlist 2 set-order definitions
\ Define Gforth's r-r-bracket if needed
[undefined] ]] [if]
: refilling-parse-name ( -- old->in c-addr u )
begin
>in @ parse-name dup 0= while
2drop drop refill 0= -39 and throw
repeat
;
: ]] ( -- )
begin
refilling-parse-name s" [[" compare while
>in ! POSTPONE postpone
repeat
drop
; immediate
[then]
0
1 cells +field it.nargs \ number of args for the constructor
1 cells +field it.nstate \ number of stack slots for iterator state
1 cells +field it.nitem \ number of stack slots per yielded item
1 cells +field it.init \ xt: ( it -- )
1 cells +field it.next \ xt: ( it -- )
1 cells +field it.drop \ xt: ( it -- )
1 cells +field it.next-back \ xt: ( it -- )
constant /iter-type
: [for-each] {: it xt -- :}
it dup it.init @ execute
]]
begin
[[ it dup it.next @ execute ]] 0<> while
[[ xt compile, ]]
repeat
[[ it it.nitem @ 0 ?do postpone drop loop ]]
[[ it dup it.drop @ execute ]]
[[
; immediate
: init-it ( nargs nstate nitem init next drop next-back a-addr -- it )
>r
r@ it.next-back !
r@ it.drop !
r@ it.next !
r@ it.init !
r@ it.nitem !
r@ it.nstate !
r@ it.nargs !
r>
;
: make-it ( nargs nstate nitem init next drop next-back -- it )
/iter-type allocate throw init-it
;
: %range-next ( lo hi -- lo' hi lo flag )
2dup = if 0 false else over >r swap 1+ swap r> true then
;
: %range-next-back ( lo hi -- lo hi' hi' flag )
2dup = if 0 false else 1- dup true then
;
: range-next ( it -- ) drop postpone %range-next ;
: range-next-back ( it -- ) drop postpone %range-next-back ;
: range-drop ( it -- ) drop postpone 2drop ;
: todo ( -- ) true abort" not yet implemented" ;
: identity ( -- ) ;
: [range-iterator] ( -- it )
2
2
1
['] drop
['] range-next
['] range-drop
['] range-next-back
make-it
; immediate
/iter-type
1 cells +field
filter.it
1 cells +field filter.xt
constant /filter
: filter-init ( it -- )
filter.it @ dup it.init @ execute ;
: %then ( orig -- ) postpone then ; immediate
: filter-next {: it -- :}
]]
begin
[[ it
filter.it @ dup it.next @ execute ]]
0= if false ahead [[ 1 cs-roll ]] then
dup [[ it filter.xt @ compile, ]] if true ahead [[ 1 cs-roll ]] then
drop
[[ 2 cs-roll ]]
again %then %then
[[
;
: filter-drop ( it -- )
filter.it @ dup it.drop @ execute ;
: make-filter ( it xt -- it2 )
/filter allocate throw >r
r@ filter.xt !
r@
filter.it !
r@
filter.it @ it.nstate @
r@
filter.it @ it.nstate @
r@
filter.it @ it.nitem @
['] filter-init
['] filter-next
['] filter-drop
['] todo
r> init-it
;
: [filter] ( it xt -- it2 ) make-filter ; immediate
/iter-type
1 cells +field
map.it
1 cells +field map.xt
constant /map
: map-next {: it -- :}
it
map.it @ dup it.next @ execute
]]
if
[[ it map.xt @ compile, ]]
true
else
[[
it
map.it @ it.nitem @ it it.nitem @ -
dup 0< if
negate 0 ?do 0 postpone literal loop
else
0 ?do postpone drop loop
then
]]
false
then
[[
;
: map-drop ( it -- )
map.it @ dup it.drop @ execute ;
: map-init ( it -- )
map.it @ dup it.init @ execute ;
: make-map ( it u xt -- it2 )
/map allocate throw >r
r@ map.xt !
r@ it.nitem !
r@
map.it !
r@
map.it @ it.nstate @
dup
r@ it.nitem @
['] map-init
['] map-next
['] map-drop
['] todo
r> init-it
;
: [map] ( it xt -- it2 ) make-map ; immediate
/iter-type
1 cells +field
reverse.it
constant /reverse
: reverse-init ( it -- )
reverse.it @ dup it.init @ execute ;
: reverse-next ( it -- )
reverse.it @ dup it.next-back @ execute ;
: reverse-drop ( it -- )
reverse.it @ dup it.drop @ execute ;
: reverse-next-back ( it -- )
reverse.it @ dup it.next @ execute ;
: make-reverse ( it -- it2 )
/reverse allocate throw >r
r@
reverse.it !
0
r@
reverse.it @ it.nstate @
r@
reverse.it @ it.nitem @
['] reverse-init
['] reverse-next
['] reverse-drop
['] reverse-next-back
r> init-it
;
: [reverse] ( it -- it2 ) make-reverse ; immediate
/iter-type
1 cells +field zip.it1
1 cells +field zip.it2
constant /zip
: zip-init ( it -- )
dup zip.it1 @ dup it.init @ execute
zip.it2 @ dup it.init @ execute
;
\ Generate code to rotate SIZE items at depth DEPTH to the top of the stack,
\ i.e. the stack effect should be: ( size×x depth×x -- depth×x size×x )
\
\ 1 1 NROLL corresponds to SWAP,
\ 2 2 NROLL to 2SWAP and
\ 1 2 NROLL to ROT.
: nroll ( size depth -- )
{: s d :}
\ s d 1 1 d= if postpone swap exit then
\ s d 2 2 d= if postpone 2swap exit then
\ s d + 1- 2 = if s 0 ?do postpone rot loop exit then
\ d 0= if exit then
s 0 ?do
s d + 1- postpone literal postpone roll
loop
;
: zip-next ( it -- )
dup zip.it1 @ swap zip.it2 @ {: it1 it2 :}
it1 it.nstate @ it2 it.nstate @ nroll
it1 dup it.next @ execute
]] if
[[
it2 it.nstate @ it1 it.nstate @ it1 it.nitem @ + nroll
it2 dup it.next @ execute
it2 it.nstate @ it2 it.nitem @ 1+ nroll
it1 it.nitem @ it2 it.nitem @ 1 + + it2 it.nstate @ nroll
]]
else
[[
it2 it.nstate @ it1 it.nitem @ nroll
it1 it.nstate @ it1 it.nitem @ + it2 it.nstate @ nroll
it2 it.nitem @ 0 ?do 0 postpone literal loop
]]
false
then
[[
;
: zip-drop ( it -- )
dup zip.it2 @ dup it.drop @ execute
zip.it1 @ dup it.drop @ execute
;
: make-zip ( it1 it2 -- it3 )
/zip allocate throw >r
r@ zip.it2 !
r@ zip.it1 !
r@ zip.it1 @ it.nstate @ r@ zip.it2 @ it.nstate @ +
dup
r@ zip.it1 @ it.nitem @ r@ zip.it2 @ it.nitem @ +
['] zip-init
['] zip-next
['] zip-drop
['] todo
r> init-it
;
: [zip] ( it1 it2 -- it3 ) make-zip ; immediate
/iter-type
1 cells +field
enumerate.it
constant /enumerate
: enumerate-init ( it -- )
enumerate.it @ dup it.init @ execute
0 postpone literal
;
: enumerate-next ( it -- )
enumerate.it @ {: it :}
it it.nstate @ 1 nroll
it dup it.next @ execute
1 it it.nstate @ it it.nitem @ 1+ + nroll
]] tuck 1+ [[
it it.nitem @ 2 + 1 nroll
;
: enumerate-drop ( it -- )
postpone drop
enumerate.it @ dup it.drop @ execute
;
: make-enumerate ( it -- it2 )
/enumerate allocate throw >r
r@
enumerate.it !
r@
enumerate.it @ it.nstate @
dup 1+
r@
enumerate.it @ it.nitem @ 1+
['] enumerate-init
['] enumerate-next
['] enumerate-drop
['] todo
r> init-it
;
: [enumerate] ( it -- it2 ) make-enumerate ; immediate
: [fold] {: it u xt -- :}
it it.nargs @ u nroll
it dup it.init @ execute
]] begin
[[ it dup it.next @ execute ]] while
[[
u it it.nstate @ it it.nitem @ + nroll
it it.nitem @ u nroll
xt compile,
it it.nstate @ u nroll
]]
repeat
[[
it it.nitem @ 0 ?do postpone drop loop
it dup it.drop @ execute
; immediate
0
1 cells +field line-iter.file
2 cells +field line-iter.buffer
constant /line-iter
: make-line-iter ( file buffer-size -- &line-iter )
/line-iter allocate throw >r
dup allocate throw swap r@ line-iter.buffer 2!
r@ line-iter.file !
r>
;
/iter-type
constant /lines
: %line-iter-next ( &line-iter -- &line-iter c-addr u flag )
dup >r
r@ line-iter.buffer 2@
over swap r> line-iter.file @ read-line
throw
;
: %line-iter-drop ( &line-iter -- )
>r
r@ line-iter.buffer 2@ drop free throw
r@ line-iter.file @ close-file throw
r> drop
;
: lines-init ( it -- ) drop postpone make-line-iter ;
: lines-next ( it -- ) drop postpone %line-iter-next ;
: lines-drop ( it -- ) drop postpone %line-iter-drop ;
: [lines-iterator] ( -- it )
2
1
2
['] lines-init
['] lines-next
['] lines-drop
['] todo
make-it
; immediate
get-current previous definitions constant iter-wordlist
\ tests
forth-wordlist iter-wordlist wordlist 3 set-order definitions
: #u ( u -- ) s>d #s 2drop bl hold ;
: test-range-iterator ( -- )
<#
0 10 [range-iterator]
[ ' #u ] [for-each]
0 0 #>
s" 9 8 7 6 5 4 3 2 1 0" compare 0<> abort" test-range-iterator failed"
;
\ see test-range-iterator
: test-reverse ( -- )
<#
0 10 [range-iterator] [reverse]
[ ' #u ] [for-each]
0 0 #>
s" 0 1 2 3 4 5 6 7 8 9" compare 0<> abort" test-reverse failed"
;
: .pair ( u1 u2 -- ) ." (" swap . 0 .r ." ) " ;
: #pair ( u1 u2 -- )
s" )" holds swap s>d #s 2drop bl hold s>d #s 2drop s" (" holds
;
: test-zip ( -- )
<#
0 10 0 4
[range-iterator] [reverse]
[range-iterator]
[zip]
[ ' #pair ] [for-each]
0 0 #>
s" (3 6) (2 7) (1 8) (0 9)" compare 0<> abort" test-zip failed"
;
: test-fold ( -- )
0 10 [range-iterator]
1234 0 [ 2 ' + ] [fold]
1234 9 10 * 2/ d= 0= abort" test-fold failed"
;
\ see test-fold
: test-map ( -- )
<#
0 5 [range-iterator]
[ 2 ' dup ] [map]
[ ' #pair ] [for-each]
0 0 #>
s" (4 4) (3 3) (2 2) (1 1) (0 0)" compare 0<> abort" test-map failed"
;
\ see test-map
: test-enumerate ( -- )
<#
10 13 [range-iterator]
[enumerate]
[ ' #pair ] [for-each]
0 0 #>
\ 2dup type
s" (2 12) (1 11) (0 10)" compare 0<> abort" test-enumerate failed"
;
\ see test-enumerate
: .line ( c-addr u line# -- ) 2 .r ." : " type ;
: test-lines ( -- )
s" /etc/motd" r/o open-file throw 256 [lines-iterator]
[enumerate]
[ 0 ' .line ] [map]
[ ' cr ] [for-each]
;
\ see test-lines
: /2? ( n -- flag ) 2 mod 0= ;
: /3? ( n -- flag ) 3 mod 0= ;
: /6? ( start end -- )
[range-iterator]
[ ' /2? ] [filter]
[ ' /3? ] [filter]
[ ' . ] [for-each]
;
\ see /6?
: test-/6? ( -- )
<#
0 50 [range-iterator]
[ ' /2? ] [filter]
[ ' /3? ] [filter]
[ ' #u ] [for-each]
0 0 #>
s" 48 42 36 30 24 18 12 6 0" compare 0<> abort" test-/6? failed"
;
: run-tests ( -- )
test-range-iterator
test-reverse
test-zip
test-fold
test-map
test-enumerate
test-lines
test-/6?
depth 0<> abort" non-zero depth"
." tests passed" cr
;
run-tests
bye