├── .gitignore ├── Gemfile ├── Rakefile ├── lib └── object_enumerate.rb ├── test └── test_enumerate.rb ├── object_enumerate.gemspec ├── examples.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/setup' 2 | require 'rubygems/tasks' 3 | Gem::Tasks.new 4 | -------------------------------------------------------------------------------- /lib/object_enumerate.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def enumerate 3 | raise ArgumentError, "No block given" unless block_given? 4 | Enumerator.new do |y| 5 | val = self 6 | y << val 7 | loop do 8 | val = yield(val) 9 | y << val 10 | end 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /test/test_enumerate.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require_relative '../lib/object_enumerate' 3 | 4 | class TestEnumerate < Test::Unit::TestCase 5 | def test_enumerate 6 | enum = 1.enumerate(&:succ) 7 | assert_instance_of(Enumerator, enum) 8 | assert_equal(nil, enum.size) 9 | assert_equal([1, 2, 3, 4], enum.take(4)) 10 | assert_raise(ArgumentError) { 1.enumerate } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /object_enumerate.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'object_enumerate' 3 | s.version = '0.0.1' 4 | s.authors = ['Victor Shepelev'] 5 | s.email = 'zverok.offline@gmail.com' 6 | s.homepage = 'https://github.com/zverok/object_enumerate' 7 | 8 | s.summary = 'Object#enumerate: Simple infinite enumerators' 9 | s.description = <<-EOF 10 | This is just a showcase gem to support language core proposal: https://bugs.ruby-lang.org/issues/14423 11 | EOF 12 | s.licenses = ['MIT'] 13 | 14 | s.files = `git ls-files lib LICENSE.txt *.md *.rb`.split($RS) 15 | s.require_paths = ["lib"] 16 | 17 | s.required_ruby_version = '>= 2.2.0' # in 2.1, MiniTest/TestUnit behaves weird 18 | 19 | s.add_development_dependency 'rake' 20 | s.add_development_dependency 'rubygems-tasks' 21 | end -------------------------------------------------------------------------------- /examples.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lib/object_enumerate' 2 | require 'pp' 3 | 4 | # Most idiomatic "infinite sequence" possible: 5 | p 1.enumerate(&:succ).take(5) 6 | 7 | # Easy Fibonacci 8 | p [0, 1].enumerate { |f0, f1| [f1, f0 + f1] }.take(10).map(&:first) 9 | 10 | require 'date' 11 | 12 | # Find next Tuesday 13 | Date.today.enumerate(&:succ).detect { |d| d.wday == 2 } 14 | 15 | require 'nokogiri' 16 | require 'open-uri' 17 | 18 | # Find some element on page, then make list of all parents 19 | p Nokogiri::HTML(open('https://www.ruby-lang.org/en/')) 20 | .at('a:contains("Ruby 2.2.10 Released")') 21 | .enumerate(&:parent) 22 | .take_while { |node| node.respond_to?(:parent) } 23 | .map(&:name) 24 | 25 | require 'octokit' 26 | 27 | Octokit.stargazers('rails/rails') 28 | # ^ this method returned just an array, but have set `.last_response` to full response, with data 29 | # and pagination. So now we can do this: 30 | p Octokit.last_response 31 | .enumerate { |response| response.rels[:next].get } # pagination: `get` fetches next Response 32 | .first(3) # take just 3 pages of stargazers 33 | .flat_map(&:data) # data is parsed response content (stargazers themselves) 34 | .map { |h| h[:login] } 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `Object#enumerate` 2 | 3 | This is a small (exactly 1 method) gem to showcase my proposal to core Ruby. 4 | 5 | [The proposal at Ruby tracker](https://bugs.ruby-lang.org/issues/14423). 6 | 7 | [Discussion log](https://docs.google.com/document/d/e/2PACX-1vR2LdBE87iEcEsVuUUr0G2L6LxSPeGMg_0oeHeh0HYmX36iIa9zkWYlFHilH5D4I_RBJpQnr09yOZaE/pub) from developers meeting: 8 | 9 | > Naruse: interesting proposal 10 | > 11 | > Akr: adding method to Object sounds too radical to me. 12 | > 13 | > Usa: I think there is a chance if this is a method of Enumerable. 14 | > 15 | > Shyouhei: my feeling is this should start as a gem. 16 | 17 | So, considering Shyouhei's last remark, I am providing this gem for interested parties to experiment. 18 | 19 | I still **strongly believe** the method should be a part of language core, so the gem is made as a proof-of-concept, to make experimentation with an idea simple. 20 | 21 | **UPD: I thought about alternative proposal, [`Enumerator#generate`](https://github.com/zverok/enumerator_generate), which seems less radical and more powerful at the same time.** 22 | 23 | ## Synopsys 24 | 25 | `Object#enumerate` takes a block and returns an instance of infinite `Enumerator`, where each next element is made by applying the block to the previous. 26 | 27 | ## Examples of usage 28 | 29 | `Object#enumerate` can provide idiomatic replacement for a lot of `while` and `loop` constructs, the same way `each` replaces `for`. 30 | 31 | ```ruby 32 | require 'object_enumerate' 33 | 34 | # Most idiomatic "infinite sequence" possible: 35 | p 1.enumerate(&:succ).take(5) 36 | # => [1, 2, 3, 4, 5] 37 | 38 | # Easy Fibonacci 39 | p [0, 1].enumerate { |f0, f1| [f1, f0 + f1] }.take(10).map(&:first) 40 | #=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34] 41 | 42 | # Find next Tuesday 43 | require 'date' 44 | Date.today.enumerate(&:succ).detect { |d| d.wday == 2 } 45 | # => # 46 | 47 | 48 | # Tree navigation 49 | # --------------- 50 | require 'nokogiri' 51 | require 'open-uri' 52 | 53 | # Find some element on page, then make list of all parents 54 | p Nokogiri::HTML(open('https://www.ruby-lang.org/en/')) 55 | .at('a:contains("Ruby 2.2.10 Released")') 56 | .enumerate(&:parent) 57 | .take_while { |node| node.respond_to?(:parent) } 58 | .map(&:name) 59 | # => ["a", "h3", "div", "div", "div", "div", "div", "div", "body", "html"] 60 | 61 | # Pagination 62 | # ---------- 63 | require 'octokit' 64 | 65 | Octokit.stargazers('rails/rails') 66 | # ^ this method returned just an array, but have set `.last_response` to full response, with data 67 | # and pagination. So now we can do this: 68 | p Octokit.last_response 69 | .enumerate { |response| response.rels[:next].get } # pagination: `get` fetches next Response 70 | .first(3) # take just 3 pages of stargazers 71 | .flat_map(&:data) # `data` is parsed response content (stargazers themselves) 72 | .map { |h| h[:login] } 73 | # => ["wycats", "brynary", "macournoyer", "topfunky", "tomtt", "jamesgolick", ... 74 | ``` 75 | 76 | ### Alternative synopsys 77 | 78 | _Not implemented in the gem, just provided for a sake of core language proposal._ 79 | 80 | ```ruby 81 | Enumerable.enumerate(1, &:succ) 82 | Enumerable(1, &:succ) 83 | Enumerator.from(1, &:succ) 84 | ``` 85 | 86 | I personally don't believe any of those look clear enough, so, however risky adding new method to `Object` could look, I'd vote for it. 87 | 88 | ## UPD: Naming 89 | 90 | _I received several responses about the name not being "obvious enough". It was not expected, I am sorry for not providing the reasons about name earlier._ 91 | 92 | The reasons behind the name `#enumerate`: 93 | 94 | * Core method names should be short and mnemonic, not long and descriptive. (Otherwise, we'd have `yield_each` instead of `each`, `yield_each_and_collect` instead of `map`, `parallel_each` instead of `zip` and don't even get me started on `reduce`). It is if you can't guess what core method _exactly_ does _just_ from the name, more important to have it easily remembered and associative. 95 | * Code constructs using the name should be spellable in your head. I pronounce it "1: enumerate succeeding numbers", "last result: enumerate all next" and so on. Judge yourself. 96 | * Concept is present in other languages and frequently named `iterate` (for example, Clojure and Scala). As we call our iterators `Enumerator`, it is logical to call the method `enumerate`. 97 | * Once you got it, the name is hard to confuse with anything (the only other slightly similar name in core is `#to_enum`, but it is typically used in a very far context). 98 | 99 | The only other reasonable option I can think about is `deduce`, as an antonym for `reduce` which makes the opposite thing. Elixir follows this way, calling the methods `fold` (for our `reduce`) and `unfold` (for method proposed). 100 | --------------------------------------------------------------------------------