coalton-lang / coalton

Coalton is an efficient, statically typed functional programming language that supercharges Common Lisp.

Home Page:https://coalton-lang.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bug: External iterators in nested `for` loops

Izaakwltn opened this issue · comments

With nested for loop, external/ parametric iterators cause unexpected behavior.

Defining iterators within each for macro gives expected results

(coalton-toplevel

  (define (permutations a-limit b-limit c-limit d-limit)
    (let results = (cell:new Nil))
    (for a in (iter:up-to a-limit)
         (for b in (iter:up-to b-limit)
              (for c in (iter:up-to c-limit)
                   (for d in (iter:up-to d-limit)
                        (cell:push! results (Tuple4 a b c d))))))
    (reverse (cell:read results))))
NESTED-LOOP-BUG> (coalton (permutations 2 2 2 2))
(#.(TUPLE4 0 0 0 0) #.(TUPLE4 0 0 0 1) #.(TUPLE4 0 0 1 0) #.(TUPLE4 0 0 1 1)
 #.(TUPLE4 0 1 0 0) #.(TUPLE4 0 1 0 1) #.(TUPLE4 0 1 1 0) #.(TUPLE4 0 1 1 1)
 #.(TUPLE4 1 0 0 0) #.(TUPLE4 1 0 0 1) #.(TUPLE4 1 0 1 0) #.(TUPLE4 1 0 1 1)
 #.(TUPLE4 1 1 0 0) #.(TUPLE4 1 1 0 1) #.(TUPLE4 1 1 1 0) #.(TUPLE4 1 1 1 1))

Defining the iterator externally and calling it as an argument causes unexpected results, quitting prematurely:

(coalton-toplevel

  (define (permutations2-backend a-iter b-iter c-iter d-iter)
    (let results = (cell:new Nil))
    (for a in a-iter
         (for b in b-iter
              (for c in c-iter
                   (for d in d-iter
                        (cell:push! results (Tuple4 a b c d))))))
    (reverse (cell:read results)))

  (define (permutations2 a-limit b-limit c-limit d-limit)
    (permutations2-backend (iter:up-to a-limit)
                           (iter:up-to b-limit)
                           (iter:up-to c-limit)
                           (iter:up-to d-limit))))
NESTED-LOOP-BUG> (coalton (permutations2 2 2 2 2))
(#.(TUPLE4 0 0 0 0) #.(TUPLE4 0 0 0 1))

It seems to work okay for the innermost level, and this isn't a problem for non-nested for loops, for example:

(coalton-toplevel

  (define (single-for-taking-iter it)
    (let results = (cell:new Nil))
    (for i in it
         (cell:push! results i))
    (reverse (cell:read results))))
NESTED-LOOP-BUG> (coalton (single-for-taking-iter (iter:up-to 7)))

(0 1 2 3 4 5 6)

(Package Definition):

(defpackage #:nested-loop-bug
  (:use #:coalton
        #:coalton-prelude)
  (:local-nicknames (#:tuple #:coalton-library/tuple)
                    (#:cell #:coalton-library/cell)
                    (#:iter #:coalton-library/iterator)))

By the way, this is also the case for iter:for-each!

I don't think this is a bug -- the issue is that the same iterator object cannot be iterated over twice.

When you call (permutations2-backend iter-a iter-b iter-c iter-d) each of those iterator arguments can only be iterated over once. You'll notice that in this example, iter-d is consumed after it produces the value 1, after that it can no longer be used. Hence there's nothing left for the loop to do, so you only get those first two values because the push operation is never called again.

But in the first example, where you are creating a fresh iterator object using the value most recently generated in the nearest outer-loop, it works as expected.

There IS an inconsistency here though. If you replace the iterator objects with objects of types that are merely into-iterator but not iterator, you get the expected result:

(coalton 
       (permutations2-backend "ab" "cd" "ef" "gh"))
(#.(TUPLE4 #\a #\c #\e #\g) #.(TUPLE4 #\a #\c #\e #\h)
 #.(TUPLE4 #\a #\c #\f #\g) #.(TUPLE4 #\a #\c #\f #\h)
 #.(TUPLE4 #\a #\d #\e #\g) #.(TUPLE4 #\a #\d #\e #\h)
 #.(TUPLE4 #\a #\d #\f #\g) #.(TUPLE4 #\a #\d #\f #\h)
 #.(TUPLE4 #\b #\c #\e #\g) #.(TUPLE4 #\b #\c #\e #\h)
 #.(TUPLE4 #\b #\c #\f #\g) #.(TUPLE4 #\b #\c #\f #\h)
 #.(TUPLE4 #\b #\d #\e #\g) #.(TUPLE4 #\b #\d #\e #\h)
 #.(TUPLE4 #\b #\d #\f #\g) #.(TUPLE4 #\b #\d #\f #\h))

So there is a disappointing inconsistency. When "ab" is turned into an iterator, a fresh iterator is made. But when the value of (iter:up-to 10) is passed to into-iterator's method, you get the exact same object back again.

For a more instructive example:

  (define (run-it) 
      (let ((x (iter:up-to 3)))
        (iter:for-each! (fn (i) (traceobject "hey" i)) x)
        (iter:for-each! (fn (i) (traceobject "nope" i)) x)))

This will print out


(coalton (run-it ))
hey: 0
hey: 1
hey: 2
COALTON::UNIT/UNIT

Thank you for the illuminating response! I forgot that iterators were single-use...

I'll close the issue

I'm closing the issue

Edit: It won't let me close the issue.

To be clear, I think that there is an "issue" here, just not a bug.

I think it is genuinely unsettling from a user perspective that, because for uses into-iter under the hood, we observe inconsistent behavior between non-ITERATOR instances of INTOITERATOR and ITERATORS