Help understanding yield and enumerators in Ruby

前端 未结 3 2045
野的像风
野的像风 2020-12-30 16:41

I would appreciate it if someone could help me understand the difference between using a Yielder in an Enumerator vs. just invoking yield in an Enumerator.

The \"Wel

3条回答
  •  醉酒成梦
    2020-12-30 17:12

    It might help if you first understand how yield works. Here is an example:

    def do_stuff
      if block_given?
        yield 5
      else
        5
      end
    end
    
    result = do_stuff {|x| x * 3 }
    puts result
    
    --output:--
    15
    

    In the the do_stuff method call:

    do_stuff {|x| x * 3 }
    

    ..the block is like a function, and it is passed to the method do_stuff. Inside do_stuff, yield calls the function and passes the specified arguments--in this case 5.

    Some important things to note:

    1. yield is called inside a method

    2. When you call a method, you can pass a block to the method

    3. yield is used to call the block.

    Okay, now let's look at your comment question:

    Is it true that

    e = Enumerator.new do |y| 
      y << 1 
      y << 2 
      y << 3 
    end 
    

    is exactly the same as

    e = Enumerator.new do   #I think you forgot to write .new here
        yield 1 
        yield 2 
        yield 3 
    end
    

    In the second example, there is no method definition anywhere--so you can't call yield. Error! Therefore, the two examples are not the same.

    However, you could do this:

    def do_stuff
      e = Enumerator.new do 
          yield 1 
          yield 2 
          yield 3 
      end 
    end
    
    my_enum = do_stuff {|x| puts x*3}
    my_enum.next
    
    --output:--
    3
    6
    9
    1.rb:12:in `next': iteration reached an end (StopIteration)
        from 1.rb:12:in `
    '

    But that is a funny enumerator because it doesn't produce any values--it just executes some code(which happens to print some output), then ends. That enumerator is almost equivalent to:

    def do_stuff
      e = Enumerator.new do 
      end 
    end
    
    my_enum = do_stuff
    my_enum.next
    
    --output:--
    1.rb:7:in `next': iteration reached an end (StopIteration)
        from 1.rb:7:in `
    '

    When an enumerator cannot produce a value, it raises a StopIteration exception. So in both cases, the enumerator couldn't produce a value.

    But it's still not clear to me what the "yielder" is doing. It looks like it is collecting all the calculated values so that it can regurgitate them later when you use the enumerator. If that's the case, then it seems like it would only be practical for "small" sequences....you wouldn't want to make an enumerator that stored 50 million items away.

    No. In fact, you can create an enumerator that produces an infinite number of values. Here is an example:

    e = Enumerator.new do |y|
      val = 1
    
      while true
        y << val
        val += 1
      end
    
    end
    
    puts e.next
    puts e.next
    puts e.next
    
    --output:--
    1
    2
    3
    

    Adding some debugging messages should prove insightful:

    e = Enumerator.new do |y|
      val = 1
    
      while true
        puts "in while loop"
        y << val
        val += 1
      end
    
    end
    
    puts e.next
    
    --output:--
    in while loop
    1
    

    Note that the message only printed once. So something is going on that is not obvious:

    e = Enumerator.new do |y|
      val = 1
    
      while true
        puts "in while loop"
        y << val
        puts "just executed y << val"
        val += 1
      end
    
    end
    
    puts e.next
    
    --output:--
    in while loop
    1
    

    Because the message "just executed y << val" does not show up in the output, that means execution must have halted on the line y << val. Therefore, the enumerator did not continuously spin the while loop and insert all the values into y--even though the syntax is exactly the same as pushing values into an array: arr << val.

    What y << val really means is: when e.next() is called produce this value, then continue execution on the next line. If you add another e.next to the previous example, you will see this additional output:

    just executed y << val
    in while loop
    2
    

    What's happening is that execution always halts when y << val is encountered in the code. Then calling e.next produces the value on the right side, then execution continues on the next line.

    It would probably have made more sense if ruby had made the syntax for the yielder statement like this:

    y >> val
    

    And we could interpret that as meaning: halt execution here, then when e.next is called produce val.

    David Black recommends not using the y.yield val syntax, which is equivalent to y << val lest readers think it works similarly to the yield statement. y.yield val should be interpreted as: "stop execution here, and when next is called produce val, then continue execution on the next line. Personally, I think that the syntax y << val stands out more than y.yield val, so it is easier to spot in the code and readily identify where execution halts.

提交回复
热议问题