Feature #8840
closedYielder#state
Added by marcandre (Marc-Andre Lafortune) about 12 years ago. Updated about 8 years ago.
Description
Defining an Enumerator that require a state is currently troublesome. For example, it is not really possible to define an equivalent of Lazy#drop in Ruby without making an assumption on the implementation.
To address this, I propose that we
(a) guarantee that a new Yielder object will be given for each enumeration
(b) add a 'state' attribute to Yielder.
This way, one could implement Lazy#drop in a way similar to:
class Enumerator::Lazy < Enumerator
def drop(n)
n = n.to_i
Lazy.new(self) do |yielder, *values|
yielder.state ||= n
if yielder.state > 0
yielder.state -= 1
else
yielder.yield(*values)
end
end
end
end
Note that (a) is currently true for Ruby MRI, JRuby and Rubinius, but it is not explicit in the documentation.
Files
        
           Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #1
            [ruby-core:56895]
          Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #1
            [ruby-core:56895]
        
      
      
    
        
           Updated by matz (Yukihiro Matsumoto) about 12 years ago
          
          
        
        
          
            Actions
          
          #2
            [ruby-core:56910]
          Updated by matz (Yukihiro Matsumoto) about 12 years ago
          
          
        
        
          
            Actions
          
          #2
            [ruby-core:56910]
        
      
      - Status changed from Open to Feedback
I understand the motivation, and how it works.  It is very simple.
But I hesitate to introduce state easily in this age of functional programming.
Let me think for a while.
And tell me if anyone has better idea to address the issue.
Matz.
        
           Updated by judofyr (Magnus Holm) about 12 years ago
          
          
        
        
          
            Actions
          
          #3
            [ruby-core:56931]
          Updated by judofyr (Magnus Holm) about 12 years ago
          
          
        
        
          
            Actions
          
          #3
            [ruby-core:56931]
        
      
      On Sat, Aug 31, 2013 at 12:52 AM, marcandre (Marc-Andre Lafortune) <
ruby-core@marc-andre.ca> wrote:
Defining an Enumerator that require a state is currently troublesome. For
example, it is not really possible to define an equivalent of Lazy#drop in
Ruby without making an assumption on the implementation.
Can't you just use the closure?
class Enumerator::Lazy < Enumerator
def drop(n)
n = n.to_i
Lazy.new(self) do |yielder, *values|
if n > 0
n -= 1
else
yielder.yield(*values)
end
end
end
end
        
           Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #4
            [ruby-core:56948]
          Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #4
            [ruby-core:56948]
        
      
      judofyr (Magnus Holm) wrote:
Can't you just use the closure?
Your example will fail if iterated a second time.
It will also not work correctly when using rewind and next. Check #7696.
        
           Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #5
            [ruby-core:56952]
          Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #5
            [ruby-core:56952]
        
      
      2013/9/1 marcandre (Marc-Andre Lafortune) ruby-core@marc-andre.ca:
Your example will fail if iterated a second time.
It will also not work correctly when usingrewindandnext. Check #7696.
How about adding a method to wrap an enumerator to add a state?
% ruby -e '
class Enumerator
def with_state(init)
Enumerator.new {|y|
state = init.dup
self.each {|v|
y.yield [state, v]
}
}
end
end
class Enumerator::Lazy
def drop2(n)
e = with_state([n])
Enumerator.new {|y|
e.each {|remain, v|
if remain[0] == 0
y.yield v
else
remain[0] -= 1
end
}
}
end
end
e = (1..42).lazy
p e.drop2(40).to_a
p e.drop2(40).to_a
'
[41, 42]
[41, 42]
--
Tanaka Akira
        
           Updated by naruse (Yui NARUSE) about 12 years ago
          
          
        
        
          
            Actions
          
          #6
            [ruby-core:57539]
          Updated by naruse (Yui NARUSE) about 12 years ago
          
          
        
        
          
            Actions
          
          #6
            [ruby-core:57539]
        
      
      Need marcandre's reply
        
           Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #7
            [ruby-core:57573]
          Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #7
            [ruby-core:57573]
        
      
      I'm sorry for my late reply, I'm way back on many things I want to do.
The proposition of with_state is interesting, but I personally find it leads to complex/convoluted solutions and is cumbersome to use. Note that the given implementation of drop2 is slightly incomplete as it needs to return a lazy enumerator, so Enumerator.new needs to be followed by a call to lazy.
        
           Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #8
            [ruby-core:57577]
          Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #8
            [ruby-core:57577]
        
      
      marcandre (Marc-Andre Lafortune) wrote:
The proposition of
with_stateis interesting, but I personally find it leads to complex/convoluted solutions and is cumbersome to use. Note that the given implementation ofdrop2is slightly incomplete as it needs to return a lazy enumerator, soEnumerator.newneeds to be followed by a call tolazy.
Would you explain the incompleteness concretely?
I couldn't understand.
        
           Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #9
            [ruby-core:57603]
          Updated by marcandre (Marc-Andre Lafortune) about 12 years ago
          
          
        
        
          
            Actions
          
          #9
            [ruby-core:57603]
        
      
      akr (Akira Tanaka) wrote:
Would you explain the incompleteness concretely?
Sure. With your code above:
e.drop2(40).map(&:odd?) # => [true, false]
# expected lazy enumerator, as with original drop:
e.drop(40).map(&:odd?)  # => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: 1..42>:drop(40)>:map> 
Here is another implementation using with_state that returns a lazy enumerator:
class Enumerator::Lazy
  def drop3(n)
    Lazy.new(with_state(remain: n)) do |y, (state, v)|
      if state[:remain] == 0
        y.yield v
      else
        state[:remain] -= 1
      end
    end
  end
end
This implementation doesn't look so bad. It's probably quite a bit slower than using a Yielder#state method though.
        
           Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #10
            [ruby-core:57701]
          Updated by akr (Akira Tanaka) about 12 years ago
          
          
        
        
          
            Actions
          
          #10
            [ruby-core:57701]
        
      
      2013/10/3 marcandre (Marc-Andre Lafortune) ruby-core@marc-andre.ca:
Issue #8840 has been updated by marcandre (Marc-Andre Lafortune).
akr (Akira Tanaka) wrote:
Would you explain the incompleteness concretely?
Sure. With your code above:
e.drop2(40).map(&:odd?) # => [true, false] # expected lazy enumerator, as with original drop: e.drop(40).map(&:odd?) # => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: 1..42>:drop(40)>:map>Here is another implementation using
with_statethat returns a lazy enumerator:class Enumerator::Lazy def drop3(n) Lazy.new(with_state(remain: n)) do |y, (state, v)| if state[:remain] == 0 y.yield v else state[:remain] -= 1 end end end endThis implementation doesn't look so bad. It's probably quite a bit slower than using a Yielder#state method though.
Thank you. I understand.
I still like with_state than Yielder#state
because it limits stateful behaviors into a method.¶
Tanaka Akira
        
           Updated by hsbt (Hiroshi SHIBATA) almost 12 years ago
          
          
        
        
          
            Actions
          
          #11
            [ruby-core:60309]
          Updated by hsbt (Hiroshi SHIBATA) almost 12 years ago
          
          
        
        
          
            Actions
          
          #11
            [ruby-core:60309]
        
      
      - Target version changed from 2.1.0 to 2.2.0
        
           Updated by akr (Akira Tanaka) over 11 years ago
          
          
        
        
          
            Actions
          
          #12
            [ruby-core:62522]
          Updated by akr (Akira Tanaka) over 11 years ago
          
          
        
        
          
            Actions
          
          #12
            [ruby-core:62522]
        
      
      I have another idea now.
How about combining Enumerator.new and Enumerator#lazy addition to closure?
class Enumerator::Lazy
  def drop4(n)
    Enumerator.new {|y|
      remain = n
      self.each {|v|
        if remain == 0
          y.yield v
        else
          remain -= 1
        end
      }
    }.lazy
  end
end
e = (1..42).lazy.drop4(40)
# e is an Enumerator::Lazy object
p e #=> #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x007f1bd457cf50>:each>>
# e.map(&:odd?) returns an Enumerator::Lazy object
p e.map(&:odd?) #=> #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator: #<Enumerator::Generator:0x007f1bd457cf50>:each>>:map>
# first e.to_a works
p e.to_a #=> [41, 42]
# second e.to_a works
p e.to_a #=> [41, 42]
# e.next and e.rewind works
p e.next #=> 41
p e.next #=> 42
e.rewind
p e.next #=> 41
p e.next #=> 42
        
           Updated by knu (Akinori MUSHA) about 8 years ago
          
          
        
        
          
            Actions
          
          #13
            [ruby-core:83488]
          Updated by knu (Akinori MUSHA) about 8 years ago
          
          
        
        
          
            Actions
          
          #13
            [ruby-core:83488]
        
      
      - Status changed from Feedback to Rejected
I guess the API is not good enough if you have to do yielder.state ||= …, and it looks like akr's suggestion works.
Since there has been no feedback, I'm closing this.