Zephyr Dodolee
Zephyr Dodolee

Reputation: 193

Does ruby support recursive enumerators?

I'm writing a method to merge two streams of numbers, and I've two alternative implementations:

def merge1(l1, l2)
    Enumerator.new do |yielder|
        h = case l1.peek <=> l2.peek
                when -1 then l1.next
                when +1 then l2.next
                else l1.next; l2.next
                end
        yielder << h
        yielder << merge1(l1, l2).next
    end.lazy              
end

def merge2(l1, l2)
    Enumerator.new do |yielder|
        loop do
            h = case l1.peek <=> l2.peek
                    when -1 then l1.next
                    when +1 then l2.next
                    else l1.next; l2.next
                    end
            yielder << h
        end
    end.lazy              
end
puts merge2((1..Float::INFINITY).lazy.map {|x| x * 2}, (1..Float::INFINITY).lazy.map {|x| x * 3}).first(10)

But merge1 only prints "2 3" while merge2 produces the correct result.

Upvotes: 4

Views: 485

Answers (2)

lightmotive
lightmotive

Reputation: 520

Rewriting the method to use yield might improve comprehension; you can then use Object#enum_for, thereby separating concerns:

  1. Merge two lists.
  2. Enumerate through that merged list.

While I would discourage recursion in this case (see alternative loop approach below), here's how that would work:

def merge1(l1, l2, &block)
  h = case l1.peek <=> l2.peek
      when -1 then l1.next
      when +1 then l2.next
      else l1.next
           l2.next
      end

  yield h

  merge1(l1, l2, &block)
end

enum_for(:merge1,
         (1..Float::INFINITY).lazy.map { |x| x * 2 }, 
         (1..Float::INFINITY).lazy.map { |x| x * 3 }
        ).lazy.first(10)

In this case, using a straight-forward loop would be a better approach than recursion because loop automatically handles StopIteration exceptions in case one passes non-infinite lists, and because you can easily use Enumerator::new...a good reminder to use the right tool for the job:

def merge(l1, l2)
  Enumerator.new do |yielder|
    loop do
      h = case l1.peek <=> l2.peek
          when -1 then l1.next
          when +1 then l2.next
          else l1.next
               l2.next
          end

      yielder << h
    end
  end
end

merge((1..5).lazy.map { |x| x * 2 }, 
      (1..5).lazy.map { |x| x * 3 }
     ).lazy.first(10).to_a
# => [2, 3, 4, 6, 8, 9, 10]
# No need to handle `StopIteration` exceptions;
# still supports infinite ranges.

Upvotes: 0

falsetru
falsetru

Reputation: 368944

You need to yield each item that sub-enumerator generate:

def merge1(l1, l2)
    Enumerator.new do |yielder|
        h = case l1.peek <=> l2.peek
                when -1 then l1.next
                when +1 then l2.next
                else l1.next; l2.next
                end
        yielder << h
        merge1(l1, l2).each do |h| # <----
          yielder << h             # <----
        end                        # <----
    end.lazy              
end

Upvotes: 4

Related Questions