blinry
blinry

Reputation: 4965

Call a block method on an iterator: each.magic.collect { ... }

I have a class with a custom each-method:

class CurseArray < Array
    def each_safe
        each do |element|
            unless element =~ /bad/
                yield element
            end
        end
    end 
end

And want to call different block methods, like "collect" or "inject" on those iterated elements. For example:

curse_array.each_safe.magic.collect {|element| "#{element} is a nice sentence."}

I know there is a specific function (which I called "magic" here) to do this, but I've forgotten. Please help! :-)

Upvotes: 4

Views: 531

Answers (3)

tokland
tokland

Reputation: 67850

The selected solution uses the common idiom to_enum :method_name unless block_given? which it's ok, but there are alternatives:

  1. Leave your "unfriendly" yielder method untouched, use enum_for when calling it.

  2. Use a lazy Enumerator.

  3. Use lazy arrays (needs Ruby 2.0 or gem enumerable-lazy).

Here's a demo code:

class CurseArray < Array
  def each_safe
    each do |element|
      unless element =~ /bad/
        yield element
      end
    end
  end 

  def each_safe2
    Enumerator.new do |enum|
      each do |element|
        unless element =~ /bad/
          enum.yield element
        end
      end
    end
  end 

  def each_safe3
    lazy.map do |element|
      unless element =~ /bad/
        element
      end
    end.reject(&:nil?)
  end 
end

xs = CurseArray.new(["good1", "bad1", "good2"])
xs.enum_for(:each_safe).select { |x| x.length > 1 }
xs.each_safe2.select { |x| x.length > 1 }
xs.each_safe3.select { |x| x.length > 1 }.to_a

Upvotes: 0

Dominik Honnef
Dominik Honnef

Reputation: 18430

The way you wrote your each_safe method, the easiest would be

curse_array.each_safe { |element| do_something_with(element) }

Edit: Oh, your each_safe method isn't correct, either. It has to be "each do", not "each.do"

Edit 2: If you really want to be able to do things like "each_safe.map", while at the same time also being able to do "each_safe { ... }" you could write your method like this:

require 'enumerator'

class CurseArray < Array
  BLACKLIST = /bad/
  def each_safe
    arr = []
    each do |element|
      unless element =~ BLACKLIST
        if block_given?
          yield element
        else
          arr << element
        end
      end
    end

    unless block_given?
      return Enumerator.new(arr)
    end
  end
end

Upvotes: 2

Sam Saffron
Sam Saffron

Reputation: 131112

If a method yields you will need to pass it a block. There is no way define a block that automatically passes itself.

Closest I can get to your spec is this:

def magic(meth)
  to_enum(meth)
end

def test
  yield 1 
  yield 2
end

magic(:test).to_a
# returns: [1,2]

The cleanest way of implementing your request is probably:

class MyArray < Array 
  def each_safe 
    return to_enum :each_safe unless block_given? 
    each{|item| yield item unless item =~ /bad/}
  end 
end

a = MyArray.new 
a << "good"; a << "bad" 
a.each_safe.to_a
# returns ["good"] 

Upvotes: 6

Related Questions