Skip to content
Prev 63153 / 63424 Next

An iteration protocol

Thank you Lionel, Peter, and Duncan!
Some responses inline below:
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.)
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.
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 <lionel at posit.co> wrote: