Enumerable::Lazy patch has been accepted into ruby trunk. This means that in ruby 2.0 we can: a = [1,2,3,4,2,5].lazy.map { |x| x * 10 }.select { |x| x > 30 } #=> a.to_a #=> [40, 50], . data.map(&:split).map(&:reverse) data.map { |l| l.split.reverse } data , Ruby passes through the data twice, generating an intermediate array. As long as the amount of data is small, this deficiency is not critical. But suppose we parse a huge file: File.open("text") do |f| f.each.flat_map(&:split).grep(/ruby/) end Enumerable::Lazy this is no longer a problem. Lazy versions of the map , select and many other functions, redefined in the Enumerable::Lazy class, do not perform the calculation immediately. Instead, they return an instance of Enumerable::Lazy , allowing you to build chains of lazy evaluation. a = [] Prime.each do |x| next if x % 4 != 3 a << x break if a.size == 10 end Prime.lazy.select {|x| x % 4 == 3 }.take(10).to_a Enumerator . Perhaps this post in 2008 was one of the first. The idea of implementation is simple and based on the remarkable property of the Enumerator objects — they can be chained together. Since then, many articles have been published , and comments have been added to enumerator.c with explanations of the idea. The debate on ruby-lang lasted more than three years, and finally Matz voted to implement Yataka Hara .Enumerable::lazy , which "wraps" the array into an instance of Enumerable::Lazy , which, in turn, is used to build a lazy chain. A patch request for C was voiced and I immediately accepted the call. By the way, I recently became interested in functional programming, and to dig into the insides of Ruby was very interesting. As a result, I was honored to request pool , which, after a minor refactoring, was adopted a few days ago. Vmerdzhili it to the trunk and it will be released in Ruby 2.0 ( see roadmap ). enum = [1, 2].each puts enum.next #=> 1 puts enum.next #=> 2 puts enum.next #=> StopIteration exception raised enum = Enumerator.new do |yielder| yielder << 1 yielder << 2 end puts enum.next #=> 1 puts enum.next #=> 2 puts enum.next #=> StopIteration exception raised enum = "xy".enum_for(:each_byte) enum.each { |b| puts b } # => 120 # => 121 o = Object.new def o.each yield yield 'hello' yield [1, 2] end enum = o.to_enum p enum.next #=> nil p enum.next #=> 'hello' p enum.next #=> [1, 2] enum = %w{foo bar baz}.map puts enum.with_index { |w, i| "#{i}:#{w}" } # => ["0:foo", "1:bar", "2:baz"] # a = [1, 2, 3] some_method(a.enum_for) [1,2,3].cycle.take(10) #=> [1, 2, 3, 1, 2, 3, 1, 2, 3, 1] #to_enum and #enum_for are in the Kernel module, which means they are available to any object. Examples are taken from enumerator.c , if you can look at it interestingly here: test / ruby / test_enumerator.rb . The enumerator's internal guitars probably deserve a separate post, but it is worth noting that all this magic with the next implemented with the help of Fiber. module Enumerable class Lazy < Enumerator def initialize(obj) super() do |yielder| obj.each do |val| if block_given? yield(yielder, val) else yielder << val end end end end def map Lazy.new(self) do |yielder, val| yielder << yield(val) end end end end a = Enumerable::Lazy.new([1,2,3]) a = a.map { |x| x * 10 }.map { |x| x - 1 } puts a.next #=> 9 puts a.next #=> 19 &block as a parameter of the method (to convert to Proc and call a call ). Instead, we can yield it right inside the block for each ! block_given? also works as it should. Moreover, being inside a block, we can still call self (like return ). Wow, isn't it? A wonderful post on this topic from Yehuda Katz ( another one ).Enumerable::Lazy#map creates a new instance of Enumerable::Lazy passing it the "previous" object as an argument. When you call to_a ( next , each with a block, take , etc.), the calculation is performed: Ruby goes back to the beginning of the chain (Fig. 1), gets the next value from the original object and returns it to the outside, sequentially modifying the lazy methods ( in the same order in which they were "hung").
super directly inside lazy_initialize I create and initialize a generator ( Enumerator::Generator ), which is passed as an argument to the enumerator.lazy_init_block_i and lazy_init_block ), and transferred the conditions to lazy_initialize .rb_ary_entry to get the yielder and the value inside the block. static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_funcall(rb_block_proc(), id_call, 1, rb_ary_entry(val, 1)); return rb_funcall(rb_ary_entry(val, 0), id_yield, 1, result); } static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_yield_values2(argc - 1, &argv[1]); return rb_funcall(argv[0], id_yield, 1, result); } Enumerator#next was called, all the processes were "applied" one by one over the next value. The lazy map and select worked fine, but when I tried to correct Enumerator#each , I realized that somehow it turns out too difficult (or not?).Enumerator::Lazy has in its arsenal select , map , reject , grep (added by the first patch), flat_map , zip , take , take_while , drop , drop_while , cycle (recently added shugo ). The development and discussion of the API is actively continuing, but if you want to get lazy right now, all you need to do is compile Ruby from the trunk and enjoy it.Enumerable::Lazy now become Enumerator::Lazysuper() in the ruby example should be called this way - with parentheses, otherwise ruby will automatically pass all arguments to itSource: https://habr.com/ru/post/140362/
All Articles