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::Lazy
super()
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