Project

General

Profile

Feature #14781

Enumerator#generate

Added by zverok (Victor Shepelev) 4 months ago. Updated 3 months ago.

Status:
Feedback
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:87222]

Description

This is alternative proposal to Object#enumerate (#14423), which was considered by many as a good idea, but with unsure naming and too radical (Object extension). This one is less radical, and, at the same time, more powerful.

Synopsys:

  • Enumerator.generate(initial, &block): produces infinite sequence where each next element is calculated by applying block to previous; initial is first sequence element;
  • Enumerator.generate(&block): the same; first element of sequence is a result of calling the block with no args.

This method allows to produce enumerators replacing a lot of common while and loop cycles in the same way #each replaces for.

Examples:

With initial value

# Infinite sequence
p Enumerator.generate(1, &:succ).take(5)
# => [1, 2, 3, 4, 5]

# Easy Fibonacci
p Enumerator.generate([0, 1]) { |f0, f1| [f1, f0 + f1] }.take(10).map(&:first)
#=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

require 'date'

# Find next Tuesday
p Enumerator.generate(Date.today, &:succ).detect { |d| d.wday == 2 }
# => #<Date: 2018-05-22 ((2458261j,0s,0n),+0s,2299161j)>

# Tree navigation
# ---------------
require 'nokogiri'
require 'open-uri'

# Find some element on page, then make list of all parents
p Nokogiri::HTML(open('https://www.ruby-lang.org/en/'))
  .at('a:contains("Ruby 2.2.10 Released")')
  .yield_self { |a| Enumerator.generate(a, &:parent) }
  .take_while { |node| node.respond_to?(:parent)  }
  .map(&:name)
# => ["a", "h3", "div", "div", "div", "div", "div", "div", "body", "html"]

# Pagination
# ----------
require 'octokit'

Octokit.stargazers('rails/rails')
# ^ this method returned just an array, but have set `.last_response` to full response, with data
# and pagination. So now we can do this:
p Enumerator.generate(Octokit.last_response) { |response| 
    response.rels[:next].get                         # pagination: `get` fetches next Response
  } 
  .first(3)                                          # take just 3 pages of stargazers
  .flat_map(&:data)                                  # `data` is parsed response content (stargazers themselves)
  .map { |h| h[:login] }
# => ["wycats", "brynary", "macournoyer", "topfunky", "tomtt", "jamesgolick", ...

Without initial value

# Random search
target = 7
p Enumerator.generate { rand(10) }.take_while { |i| i != target }.to_a
# => [0, 6, 3, 5,....]

# External while condition
require 'strscan'
scanner = StringScanner.new('7+38/6')
p Enumerator.generate { scanner.scan(%r{\d+|[-+*/]}) }.take_while { !scanner.eos? }.to_a
# => ["7", "+", "38", "/"]

# Potential message loop system:
Enumerator.generate { Message.receive }.take_while { |msg| msg != :exit }

Reference implementation: https://github.com/zverok/enumerator_generate

I want to thank all peers that participated in the discussion here, on Twitter and Reddit.

History

#1 [ruby-core:87226] Updated by shevegen (Robert A. Heiler) 4 months ago

I agree with the proposal and name.

I would like to recommend and suggest you to add it to the next ruby
developer meeting for matz' to have a look at and decide. (I think most
people already commented on the other linked suggestion, so I assume
that the issue here will remain fairly small.)

By the way props on the examples given; it's a very clean proposal,
much cleaner than any proposals I ever did myself :D, and it can be
taken almost as-is for the official documentation, IMO. :)

Now of course we have to wait and see what matz and the other core
devs have to say about it.

Here is the link to the latest developer meeting:

https://bugs.ruby-lang.org/issues/14769

#2 [ruby-core:87553] Updated by knu (Akinori MUSHA) 3 months ago

What about adding support for ending an iteration from a given block itself by raising StopIteration, rather than having to chain it with take_while?

#3 [ruby-core:87555] Updated by knu (Akinori MUSHA) 3 months ago

In today's developer meeting, we kind of loved the functionality, but haven't reached a conclusion about the name.

Some candidates:

  • Enumerator.iterate(initial = nil) { |x| ... }
    Haskell has a similar function named iterate.

  • Enumerator.from(initial) { |x| ... }
    This would sound natural when the initial value is mandatory.

#4 [ruby-core:87556] Updated by matz (Yukihiro Matsumoto) 3 months ago

  • Status changed from Open to Feedback

I am not fully satisfied with the name generate since the word does not always imply sequence generation. If someone has better name proposal, I welcome.

Matz.

#5 [ruby-core:87560] Updated by sawa (Tsuyoshi Sawada) 3 months ago

I propose the following:

  • Enumerator.sequence
  • Enumerator.recur

#6 [ruby-core:87565] Updated by zverok (Victor Shepelev) 3 months ago

I like #sequence, too.

#7 [ruby-core:87566] Updated by zverok (Victor Shepelev) 3 months ago

Though, I should add that Enumerator.generate (seen this way, not just .generate alone) seems to clearly state "generate enumerator" :)

#8 [ruby-core:87567] Updated by mame (Yusuke Endoh) 3 months ago

zverok (Victor Shepelev) wrote:

Though, I should add that Enumerator.generate (seen this way, not just .generate alone) seems to clearly state "generate enumerator" :)

"generate" seems too general. It looks the most typical or primitive way to create an enumerator, but it is not.

Haskell provides "iterate" function for this feature, but it resembles an iterator in Ruby.

#9 [ruby-core:87569] Updated by Eregon (Benoit Daloze) 3 months ago

I like Enumerator.generate, since it's really to generate a lazy sequence, to generate an Enumerator, from a block.

In the end it is basically as powerful as Enumerator.new, so I see no problem to have a factory/constructor-like name.

Enumerator.sequence sounds like it could be an eager sequence, and doesn't tell me the block is generating the next value, so I don't like it.

Enumerator.iterate sounds like we would iterate something, but we don't, we generate a sequence lazily.
The iteration itself is done by #each, not "Enumerator.iterate".
I think it only works well in Haskell due to their first-class functions, but even then "iterating" (repeated applications of) a function doesn't sound clear to me or map well to Ruby.

#10 Updated by matz (Yukihiro Matsumoto) 3 months ago

I don't like recur. Probably it came from recurrence but programmers usually think of recursive because they see recursive more often. FYI, the word recur is used in Clojure for the recursive purpose. I don't like iterate either, as @eregon stated.

Enumerator.generate may work because it generates Enumerator in a fashion different from Enumerator.new.

Matz.

#11 [ruby-core:87576] Updated by mame (Yusuke Endoh) 3 months ago

Ah, I meant iterate is not a good name for ruby. Sorry for the confusion.

#12 [ruby-core:87589] Updated by knu (Akinori MUSHA) 3 months ago

I'm not very fond of generate because it's not the only way to generate an Enumerator. There could be more to come.

Also available in: Atom PDF