Search code examples
rubyiterationrangelazy-evaluation

How to lazy concat Ruby ranges?


I have a very large range to iterate through and find the first element that satisfies specific constraints. That can be done efficiently in Ruby already.

# Runs until memory is exhausted _without_ lazy!
(1..).lazy.select { |i| i > 5 }.first
# => 6

In my use-case however, I want to begin iteration at a random interval of the range and, if no element passes the check when reaching the end of the range, continue from the start of the range (up until the random interval is reached again, if need be). With Combining two different 'ranges' to one in ruby as reference I came to...

letter = ('b'..'y').to_a.sample
[*letter..'z', *'a'...letter].map { |c| c.capitalize }.join
# => "FGHIJKLMNOPQRSTUVWXYZABCDE"

Of course, I don't have the alphabet as range to iterate through, this is just the small-scale example, which fails for my use-case.

  • the * (splat) operator is not lazy
  • map is not lazy

With some more googling and experimentation, I came to the following constructs:

# lazy version of previous alphabet example
[(letter..'z'), ('a'...letter)].lazy.flat_map { |r| r.each.lazy }.map { |c| c.capitalize }.force.join
=> "FGHIJKLMNOPQRSTUVWXYZABCDE"

# Comparable to what I want
start = rand(2**64)
# => 15282219649142738977
[(start..2**64), (0...start)].lazy.flat_map { |r| r.each.lazy }.select { |i| i % 7 == 0 }.first(5)
# => [15282219649142738978, 15282219649142738985, 15282219649142738992, 15282219649142738999, 15282219649142739006]
iter = [(start..2**64), (0...start)].lazy.flat_map { |r| r.each.lazy }.select { |i| i % 7 == 0 }
# => #<Enumerator::Lazy: #<Enumerator::Lazy: #<Enumerator::Lazy: [15282219649142738977..18446744073709551616, 0...15282219649142738977]>:flat_map>:select>
iter.next
# => 15282219649142738978
iter.next
# => 15282219649142738985

That does look overly complicated to me and maybe someone has a better idea?

Thank you for your time,
Xavier.


Solution

  • You can use Enumerable#chain

    >> start = rand(2**64)
    => 9019096319891825624
    
    >> (start..2**64).chain(0...start).lazy.select { |i| i % 7 == 0 }.first(3)
    => [9019096319891825629, 9019096319891825636, 9019096319891825643]
    
    >> Enumerator::Chain.new(start..2**64, 0...start).lazy.select { |i| i % 7 == 0 }.first(3)
    => [9019096319891825629, 9019096319891825636, 9019096319891825643]