45

I am new to Ruby, is there a way to yield values from Ruby functions? If yes, how? If not, what are my options to write lazy code?

bodacydo
  • 75,521
  • 93
  • 229
  • 319

5 Answers5

61

Ruby's yield keyword is something very different from the Python keyword with the same name, so don't be confused by it. Ruby's yield keyword is syntactic sugar for calling a block associated with a method.

The closest equivalent is Ruby's Enumerator class. For example, the equivalent of the Python:

def eternal_sequence():
  i = 0
  while True:
    yield i
    i += 1

is this:

def eternal_sequence
  Enumerator.new do |enum|
    i = 0
    while true
      enum.yield i # <- Notice that this is the yield method of the enumerator, not the yield keyword
      i +=1
    end
  end
end

You can also create Enumerators for existing enumeration methods with enum_for. For example, ('a'..'z').enum_for(:each_with_index) gives you an enumerator of the lowercase letters along with their place in the alphabet. You get this for free with the standard Enumerable methods like each_with_index in 1.9, so you can just write ('a'..'z').each_with_index to get the enumerator.

Daniel Schobel
  • 498
  • 4
  • 12
Chuck
  • 234,037
  • 30
  • 302
  • 389
26

I've seen Fibers used in that way, look at an example from this article:

fib = Fiber.new do  
  x, y = 0, 1 
  loop do  
    Fiber.yield y 
    x,y = y,x+y 
  end 
end 
20.times { puts fib.resume }
Michael Kohl
  • 66,324
  • 14
  • 138
  • 158
  • 8
    In fact, in Ruby 1.9's `Enumerator` is implemented using `Fiber`. That was one of the main reasons for adding them, actually, because in Ruby 1.8 `Enumerator`s use continuations, but that is a) rather unwieldy and b) at that time continuations were going to be removed from the Ruby language. – Jörg W Mittag Mar 24 '10 at 09:38
15

If you are looking to lazily generate values, @Chuck's answer is the correct one.

If you are looking to lazily iterate over a collection, Ruby 2.0 introduced the new .lazy enumerator.

range = 1..Float::INFINITY
puts range.map { |x| x+1 }.first(10) #  infinite loop
puts range.lazy.map { |x| x+1 }.first(10) #  [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Martin Konecny
  • 57,827
  • 19
  • 139
  • 159
7

Ruby supports generators out of the box using Enumerable::Generator:

require 'generator'

# Generator from an Enumerable object
g = Generator.new(['A', 'B', 'C', 'Z'])

while g.next?
  puts g.next
end

# Generator from a block
g = Generator.new { |g|
  for i in 'A'..'C'
    g.yield i
  end

  g.yield 'Z'
}

# The same result as above
while g.next?
  puts g.next
end

https://ruby-doc.org/stdlib-1.8.7/libdoc/generator/rdoc/Generator.html

Ersin Akinci
  • 164
  • 1
  • 6
0

Class Enumerator and its method next behave similar https://docs.ruby-lang.org/en/3.1/Enumerator.html#method-i-next

range = 1..Float::INFINITY
enumerator = range.each

puts enumerator.class # => Enumerator

puts enumerator.next  # => 1
puts enumerator.next  # => 2
puts enumerator.next  # => 3
Artem P
  • 138
  • 7