31

How can I get a lazy array in Ruby?

In Haskell, I can talk about [1..], which is an infinite list, lazily generated as needed. I can also do things like iterate (+2) 0, which applies whatever function I give it to generate a lazy list. In this case, it would give me all even numbers.

I'm sure I can do such things in Ruby, but can't seem to work out how.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
carlfilips
  • 2,574
  • 3
  • 21
  • 27
  • 2
    Regarding lazy arrays: Arrays are significantly different from lists. An implementation of lazy arrays which would allow infinite arrays, would have horrible run-time properties. – sepp2k Aug 03 '10 at 09:19

9 Answers9

42

With Ruby 1.9 you can use the Enumerator class. This is an example from the docs:

  fib = Enumerator.new { |y|
    a = b = 1
    loop {
      y << a
      a, b = b, a + b
    }
  }

  p fib.take(10) #=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Also, this is a nice trick:

  Infinity = 1.0/0

  range = 5..Infinity
  p range.take(10) #=> [5, 6, 7, 8, 9, 10, 11, 12, 13, 14]

This one only works for consecutive values though.

carlfilips
  • 2,574
  • 3
  • 21
  • 27
  • 6
    they dont have to be consecutive: (10..100).step(20).take(5) #=> [10, 20, 30, 40, 50] – horseyguy Aug 03 '10 at 00:45
  • 2
    In Ruby 1.8, you can `require 'backports'` to get this functionality too :-) – Marc-André Lafortune Aug 03 '10 at 00:56
  • 2
    Note though that doing `fib.map {|x| x+1}.take(10)` will not work, because map will try to create an array. Also note that if you do `fib.take(10)` twice, the elements will be calculated twice (unlike lazy lists where elements are kept in memory once they're calculated). So this isn't exactly equivalent to lazy lists. – sepp2k Aug 03 '10 at 09:22
  • 2
    In order to do the Enumerator equivalent of `fib.map`, you'd instead do `fib.enum_for(:map)`. – Chuck Aug 04 '10 at 00:26
  • 1
    Upvoted. Enumerator and Infinity are new to me in Ruby. I was looking up some Haskell stuff for the first time today, so it's neat to see some similar syntax for expressing similar concepts in Ruby (ex. take) – KChaloux May 29 '12 at 20:18
  • 1
    You can also use `range = 5..Float::INFINITY` in the second example. – Thai Jul 23 '14 at 15:16
  • Since Ruby 2.6, `5..` (without end) is valid (no `Infinity` or `5.step` trick needed). – Amadan Jan 11 '19 at 10:10
23

Recently Enumerable::Lazy has been added to ruby trunk. We'll see it in ruby 2.0. In particular:

a = data.lazy.map(&:split).map(&:reverse)

will not be evaluated immediately.
The result is instance of Enumerable::Lazy, that can be lazy chained any further. If you want to get an actual result - use #to_a, #take(n) (#take is now lazy too, use #to_a or #force), etc.
If you want more on this topic and my C patch - see my blog post Ruby 2.0 Enumerable::Lazy

gregolsen
  • 925
  • 9
  • 12
6

Lazy range (natural numbers):

Inf = 1.0/0.0
(1..Inf).take(3) #=> [1, 2, 3]

Lazy range (even numbers):

(0..Inf).step(2).take(5) #=> [0, 2, 4, 6, 8]

Note, you can also extend Enumerable with some methods to make working with lazy ranges (and so on) more convenient:

module Enumerable
  def lazy_select
    Enumerator.new do |yielder|
      each do |obj|
        yielder.yield(obj) if yield(obj)
      end
    end
  end
end

# first 4 even numbers
(1..Inf).lazy_select { |v| v.even? }.take(4)

output:
[2, 4, 6, 8]

More info here: http://banisterfiend.wordpress.com/2009/10/02/wtf-infinite-ranges-in-ruby/

There are also implementations of lazy_map, and lazy_select for the Enumeratorclass that can be found here: http://www.michaelharrison.ws/weblog/?p=163

horseyguy
  • 29,455
  • 20
  • 103
  • 145
  • Note that, like with the enumerator solution, you can't do things like `infinite_range.filter {|x| f(x)}.take(5)`, so it doesn't behave like lazy lists. – sepp2k Aug 03 '10 at 13:45
  • @sepp2k, added link to site that has implementations of `lazy_select` etc, for `Enumerator` class – horseyguy Aug 04 '10 at 00:14
4

In Ruby 2.0.0, they were introduced new method "Lazy" in Enumerable class.

You can check the lazy function core and usage here..

http://www.ruby-doc.org/core-2.0/Enumerator/Lazy.html
https://github.com/yhara/enumerable-lazy
http://shugomaeda.blogspot.in/2012/03/enumerablelazy-and-its-benefits.html

Mr. Black
  • 11,692
  • 13
  • 60
  • 85
2

I surprised no one answered this question appropriately yet

So, recently I found this method Enumerator.produce which in conjunction with .lazy does exactly what you described but in ruby-ish fashion

Examples

Enumerator.produce(0) do  
   _1 + 2
end.lazy
  .map(&:to_r) 
  .take(1_000)
  .inject(&:+)
# => (999000/1)

def fact(n)
  = Enumerator.produce(1) do  
    _1 + 1
  end.lazy
  .take(n)
  .inject(&:*)

fact 6 # => 720 
1

This will loop to infinity:

0.step{|i| puts i}

This will loop to infinity twice as fast:

0.step(nil, 2){|i| puts i}

This will go to infinity, only if you want it to (results in an Enumerator).

table_of_3 = 0.step(nil, 3)
steenslag
  • 79,051
  • 16
  • 138
  • 171
1

As I already said in my comments, implementing such a thing as lazy arrays wouldn't be sensible.

Using Enumerable instead can work nicely in some situations, but differs from lazy lists in some points: methods like map and filter won't be evaluated lazily (so they won't work on infinite enumerables) and elements that have been calculated once aren't stored, so if you access an element twice, it's calculated twice.

If you want the exact behavior of haskell's lazy lists in ruby, there's a lazylist gem which implements lazy lists.

sepp2k
  • 363,768
  • 54
  • 674
  • 675
0

The right answer has already identified the "lazy" method, but the example provided was not too useful. I will give a better example of when it is appropriate to use lazy with arrays. As stated, lazy is defined as an instance method of the module Enumerable, and it works on EITHER objects that implement Enumerable module (e.g. arrays - [].lazy) or enumerators which are the return value of iterators in the enumerable module (e.g. each_slice - [].each_slice(2).lazy). Note that in Enumerable module, some of the instance methods return more primitive values like true or false, some return collections like arrays and some return enumerators. Some return enumerators if a block is not given.

But for our example, the IO class also has an iterator each_line, which returns an enumerator and thus can be used with "lazy". The beautiful thing about returning an enumerator is that it does not actually load the collection (e.g. large array) in memory that it is working on. Rather, it has a pointer to the collection and then stories the algorithm (e.g. each_slice(2)) that it will use on that collection, when you want to process the collection with something like to_a, for example.

So if you are working with an enumerator for a huge performance boost, now you can attach lazy to the enumerator. So instead of iterating through an entire collection to match this condition:

file.each_line.select { |line| line.size == 5 }.first(5)

You can invoke lazy:

file.each_line.lazy.select { |line| line.size == 5 }.first(5)

If we're scanning a large text file for the first 5 matches, then once we find the 5 matches, there is no need to proceed the execution. Hence, the power of lazy with any type of enumerable object.

ian
  • 12,003
  • 9
  • 51
  • 107
Daniel Viglione
  • 8,014
  • 9
  • 67
  • 101
-4

Ruby Arrays dynamically expand as needed. You can apply blocks to them to return things like even numbers.

array = []
array.size # => 0
array[0] # => nil
array[9999] # => nil
array << 1
array.size # => 1
array << 2 << 3 << 4
array.size # => 4

array = (0..9).to_a
array.select do |e|
  e % 2 == 0
end

# => [0,2,4,6,8]

Does this help?

carlfilips
  • 2,574
  • 3
  • 21
  • 27
  • 2
    Wasn't the question about *lazy* generation of objects? The correct solution is not about arrays (which are always completely determined in Ruby) but about other types of `Enumerable`, as in Sanjana's answer. – Marc-André Lafortune Aug 03 '10 at 00:58
  • I just started ruby, I thought this was correct. I will fix this -- may your wishes come true – carlfilips Aug 03 '10 at 01:03