Feature #20664
openAdd `before` and `until` options to Enumerator.produce
Description
Enumerator.produce provides a nice way to generate an infinite sequence but is a bit awkward to define how to end a sequence. It lacks a simple and easy way to produce typical finite sequences in an intuitive syntax.
This proposal attempts to solve the problem by adding these two options to the method:
-
before
: when provided, it is used as a predicate to determine if an iteration should end before a generated value gets yielded. -
until
: when provided, it is used as a predicate to determine if an iteration should end until after a generated value gets yielded.
Any value that responds to to_proc
and returns a Proc
object is accepted in these options.
A typical use case for the before
option is traversing a tree structure to iterate over the ancestors or following/preceding siblings of a node.
The until
option can be used when there is a clear definition of the "last" value to yield.
enum = Enumerator.produce(File, before: :nil?, &:superclass)
enum.to_a #=> [File, IO, Object, BasicObject]
enum = Enumerator.produce(3, until: :zero?, &:pred)
enum_to_a #=> [3, 2, 1, 0]
Files
Updated by knu (Akinori MUSHA) 10 days ago
- Related to Feature #14781: Enumerator.generate added
Updated by knu (Akinori MUSHA) 10 days ago
- Related to Feature #20625: Object#chain_of added
Updated by zverok (Victor Shepelev) 10 days ago
I am not sure about this API.
I think in language core there aren’t many APIs that accept just a symbol of a necessary method (only reduce(:+)
comes to mind, and I am still not sure why this form exists, because it seems to have been introduced at the same time when Symbol#to_proc
was, so reduce(:+)
and reduce(&:+)
were always co-existing).
Mostly callables are passed as a block (and therefore there can be only one); but some APIs accept another callable (any object with #call
method, like Enumerator.new).
So, what if condition is not an method of the sequence?.. Should we accept callables, too? Or, what if the method’s user expects it to be a particular value (like until: 0
), or a pattern (like before: 0..1
).
The alternative is
Enumerator.produce(File, &:superclass).take_until(&:nil?)
...which is more or less the same character-count-wise, more powerful (any block can be used), and more atomic.
The one problem we don’t currently have neither Enumerable#take_until
, nor Object#not_nil?
, to write something like
# this wouldn’t work
Enumerator.produce(File, &:superclass).take_while(&:not_nil?)
# though one can use
Enumerator.produce(File, &:superclass).take_while(&:itself)
#=> [File, IO, Object, BasicObject]
...but in general, I suspect adding Enumerable#take_until
to handle such cases (and #take_while_after
while we are on it :)) might be more powerful addition to the language, useful in many situations.
Updated by knu (Akinori MUSHA) 10 days ago
This proposal is based on the potential use cases I have experienced over the years. I've rarely seen a need for infinite sequences that can be defined with produce, and that is why I want to give produce() a feature-complete constructor.
Almost all sequences have had clear and simple end conditions. Traversing a tree structure for ancestor or sibling nodes would be the most typical use case, and the predicates like nil?
and root?
are mostly enough. Type-based conditions and inclusion conditions are not much seen probably because sequences are likely to be homogeneous and there is rarely more than one or a range of terminal values.
Updated by knu (Akinori MUSHA) 10 days ago
These options should take callables in this proposal. Procs and Methods certainly meet the condition: "Any value that responds to to_proc and returns a Proc object is accepted in these options".
The implementation does not bother to call to_proc
on Procs, though.