Project

General

Profile

Feature #14781

Updated by zverok (Victor Shepelev) almost 2 years ago

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 

 ```ruby 
 # 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 

 ```ruby 
 # 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+|[-+*/]}) }.slice_after }.take_while { scanner.eos? }.first !scanner.eos? }.to_a 
 # => ["7", "+", "38", "/", "6"] "/"] 

 # 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.

Back