Thank you Lionel, Peter, and Duncan!
Some responses inline below:

> Couldn't this all be done in a while or repeat loop? ...
> Not as simple as yours, but I think a little clearer because it's more 
> concrete, less abstract.

Indeed, that’s the trade-off! Explicit and verbose vs. simple,
concise, and abstracted away. There are certainly times when I prefer
the former, but the latter is not even an option today. Particularly
in a teaching context, I think the concept of iteration is more
intuitive and faster to teach than the precise mechanics of iteration.
The opportunity to make `for` usable with a broader set of object
types is icing on the cake. (Some of these arguments are fleshed out
further in the README linked in the first email.)

> It's not clear to me how the for() loop chooses a value to pass to the 
> iterator function.

In the draft patch, `for` creates a unique sentinel object, a bare
`OBJSXP`. The iterator closure is called with this sentinel as the
argument, and the closure must return exactly it to indicate
exhaustion.

This approach neatly achieves a few design goals. It introduces no
persistent symbols, keeping the API surface small, and avoids
introducing the ugly edge case of a potential false-positive
exhaustion detection. It has less overhead than a signal. Compared to
a signal, it should also encourage a more local coding style, making
code easier to reason about. Treating errors as values is one idea
that Rust has proven the value of to me, and this value-sentinel
approach is a close cousin of that.

The example `SampleSequence` iterator in the initial email had a
default sentinel value of `NULL`. This was to allow convenient manual
iteration with something like:

```r
it <- SampleSequence(9)
it(); it(); it(); ...
```

Or, if you prefer a more explicit approach:

```r
it <- SampleSequence(9)
repeat { val <- it() %||% break; ... }
```

Or:

```r
repeat { val <- it(break); ... }
```

Or:

```r
while (!is.null(val <- it())) { ... }
```

Or, for maximum robustness:

```r
done_sentinel <- new.env(parent = emptyenv())
while (!identical(done_sentinel, val <- it(done_sentinel))) { ... }
```

This enables a variety of usage patterns with different trade-offs
between convenience and robustness, with `for` able to take the most
robust approach, while allowing the iterator’s default sentinel to
prioritize convenience.

> It's very useful to *close* iterators for resource cleanup.

This is interesting and, to be honest, not a use case we had considered.

Would using `reg.finalizer()` be sufficient for your use case? It
gives less control over timing than `on.exit()`, but can close
resources with something like:

```r
Stream <- function() {
  r <- open_resource()
  reg.finalizer(environment(), \(e) r$close())
  \(done) r$get_next() %||% done
}
```

On Tue, Aug 12, 2025 at 5:20 AM Lionel Henry <lio...@posit.co> wrote:
>
> Clever! If going for non-local returns, probably best for ergonomics to pass 
> in
> a closure (see e.g. `callCC()`). If only to avoid accidental jumps while
> debugging.
>
> But... do we need more lazy evaluation tricks in the language or fewer? It's
> probably more idiomatic to express non-local returns with condition signals
> like `stopIteration()`.
>
> There's something to be said for explicit and simple control flow though, via
> handling of returned values.
>
>
> > Note that it is trivial to create a unique sentinel value -- any newly
> > created closure (i.e. function() NULL) will do, as it will only
> > compare identical() with itself.
>
> Until you try that in the global env right? Then the risk of collision 
> slightly
> increases. Unless you make your closure more unique via `body()`, but then 
> might
> as well use a conventional sentinel.
>
> Best,
> Lionel
>
> On Tue, Aug 12, 2025 at 1:45 AM Peter Meilstrup
> <peter.meilst...@gmail.com> wrote:
> >
> > Passing the sentinel value as an argument to the iteration method is
> > the approach taken in my package `iterors` on CRAN. If the sentinel
> > value argument is evaluated lazily, this lets you pass calls to things
> > like 'stop', 'break' or 'return,' which will be called to signal end
> > of iteration. This makes for some nice compact and performant
> > iteration idioms:
> >
> > iter <- as.iteror(obj)
> > total <- 0
> > repeat {total <- total + nextOr(iter, break)}
> >
> > Note that iteror is just a closure with one optional argument and a
> > class attribute, so you can skip using s3 nextOr method and call it
> > directly:
> >
> > nextElem <- as.iteror(obj)
> > repeat {total <- total + nextElem(break)}
> >
> > For backward compatibility with the iterators package, the default
> > sentinel value for iterors is `stop("StopIteration")`.
> >
> > Note that it is trivial to create a unique sentinel value -- any newly
> > created closure (i.e. function() NULL) will do, as it will only
> > compare identical() with itself.
> >
> > sigil <- \() NULL
> > next <- as.iteror(obj)
> > while (!identical(item <-next(sigil), sigil)) {
> >   doStuff(item)
> > }
> >
> > Peter Meilstrup
> >
> > On Mon, Aug 11, 2025 at 5:56 PM Lionel Henry via R-devel
> > <r-devel@r-project.org> wrote:
> > >
> > > Hello,
> > >
> > > A couple of comments:
> > >
> > > - Regarding the closure + sentinel approach, also implemented in coro
> > >   (https://github.com/r-lib/coro/blob/main/R/iterator.R), it's more
> > > robust for the
> > >   sentinel to always be a temporary value. If you store the sentinel
> > > in a list or
> > >   a namespace, it might inadvertently close iterators when iterating over 
> > > that
> > >   collection. That's why the coro sentinel is created with 
> > > `coro::exhausted()`
> > >   rather than exported from the namespace as a constant object. The 
> > > sentinel can
> > >   be equivalently created with `as.symbol(".__exhausted__.")`, the main 
> > > thing to
> > >   ensure robustness is to avoid storing it and always create it from 
> > > scratch.
> > >
> > >   The approach of passing the sentinel by argument (which I see in the 
> > > example
> > >   in your mail but not in the linked documentation of approach 3) also
> > > works if the
> > >   iterator loop passes a unique sentinel. Having a default of `NULL` 
> > > makes it
> > >   likely to get unexpected exhaustion of iterators when a sentinel is not 
> > > passed
> > >   in though.
> > >
> > > - It's very useful to _close_ iterators for resource cleanup. It's the
> > > responsibility of an iterator loop (e.g. `for` but could be other custom 
> > > tools
> > > invoking the iterator) to close them. See 
> > > https://github.com/r-lib/coro/pull/58
> > > for an interesting application of iterator closing, allowing robust 
> > > support of
> > > `on.exit()` expressions in coro generators.
> > >
> > >   To implement iterator closing with the closure approach, an iterator may
> > >   optionally take a `close` argument. A `true` value is passed on exit,
> > >   instructing the iterator to clean up resources.
> > >
> > > Best,
> > > Lionel
> > >
> > > On Mon, Aug 11, 2025 at 3:24 PM Tomasz Kalinowski <kalinows...@gmail.com> 
> > > wrote:
> > > >
> > > > Hi all,
> > > >
> > > > A while back, Hadley and I explored what an iteration protocol for R
> > > > might look like. We worked through motivations, design choices, and edge
> > > > cases, which we documented here:
> > > > https://github.com/t-kalinowski/r-iterator-ideas
> > > >
> > > > At the end of this process, I put together a patch to R (with tests) and
> > > > would like to invite feedback from R Core and the broader community:
> > > > https://github.com/r-devel/r-svn/pull/130/files?diff=unified&w=1
> > > >
> > > > In summary, the overall design is a minimal patch. It introduces no
> > > > breaking changes and essentially no new overhead. There are two parts.
> > > >
> > > > 1.  Add a new `as.iterable()` S3 generic, with a default identity
> > > >     method. This provides a user-extensible mechanism for selectively
> > > >     changing the iteration behavior for some object types passed to
> > > >     `for`. `as.iterable()` methods are expected to return anything that
> > > >     `for` can handle directly, namely, vectors or pairlists, or (new) a
> > > >     closure.
> > > >
> > > > 2.  `for` gains the ability to accept a closure for the iterable
> > > >     argument. A closure is called repeatedly for each loop iteration
> > > >     until the closure returns an `exhausted` sentinel value, which it
> > > >     received as an input argument.
> > > >
> > > > Here is a small example of using the iteration protocol to implement a
> > > > sequence of random samples:
> > > >
> > > > ``` r
> > > > SampleSequence <- function(n) {
> > > >   i <- 0
> > > >   function(done = NULL) {
> > > >     if (i >= n) {
> > > >       return(done)
> > > >     }
> > > >     i <<- i + 1
> > > >     runif(1)
> > > >   }
> > > > }
> > > >
> > > > for(sample in SampleSequence(2)) {
> > > >   print(sample)
> > > > }
> > > >
> > > > # [1] 0.7677586
> > > > # [1] 0.355592
> > > > ```
> > > >
> > > > Best,
> > > > Tomasz
> > > >
> > > > ______________________________________________
> > > > R-devel@r-project.org mailing list
> > > > https://stat.ethz.ch/mailman/listinfo/r-devel
> > >
> > > ______________________________________________
> > > R-devel@r-project.org mailing list
> > > https://stat.ethz.ch/mailman/listinfo/r-devel

______________________________________________
R-devel@r-project.org mailing list
https://stat.ethz.ch/mailman/listinfo/r-devel

Reply via email to