On episode 54 of the Ruby Rogues podcast, Josh Susser mentioned an interview question he used to use where he would ask interviewees to reimplement Enumerable’s *ect methods (collect, select, reject, and detect) using inject.

On the same show, James Edward Gray aptly pointed out that Ruby 1.9 has a new method that works a lot like inject, but has a more meaningul name and removes the need to expliticly return the passed object during each pass: each_with_object.

I thought it’d be a fun exercise to give Josh’s interview question a go, but using each_with_object instead of inject.

Tests First

We’ll be monkey patching the Enumerable module, but first let’s get some tests in place so we know if our overrides actually work. Here’s a quick test suite, which could definitely be more thorough, but oh well this is just for fun anyhow:

require "minitest/autorun"

class StarEctTest < MiniTest::Unit::TestCase
  def setup
    @array = [1, 2, 3, 4, 5]
  end

  def test_collect
    assert_equal [2, 4, 6, 8, 10], @array.collect { |i| i * 2 }
  end

  def test_detect
    assert_equal 3, @array.detect { |i| i > 2 }
    assert_nil @array.detect { |i| i < 0 }
  end

  def test_select
    assert_equal [1, 2], @array.select { |i| i < 3 }
    assert_equal [],  @array.select { |i| i < 0 }
  end

  def test_reject
    assert_equal [3, 4, 5], @array.reject { |i| i < 3 }
    assert_equal [], @array.reject { |i| i > 0 }
  end
end

At this point we’re just testing Ruby’s imlpementation so it’s no surprise that they’re all green. Now we’ll re-open the Enumerable module and define our methods inside.

My full source code is here, in case you want to follow along at home.

Collect

collect invokes the given block on each item and returns a new array with the results. If you’ve never heard of collect, you may know it as map. Implementing it using each_with_object is pretty trivial. You just pass in an array and push processed items on to it.

def collect(&block)
  each_with_object([]) { |i, obj| obj << block.call(i) }
end

Select

select returns a new array holding just the items for which the given block returned true. This one is also pretty easy.

def select(&block)
  each_with_object([]) { |i, obj| obj << i if block.call(i) }
end

Reject

reject is just like select, only the opposite: it returns a new array holding only the items for which the given block returned false.

def reject(&block)
  each_with_object([]) { |i, obj| obj << i unless block.call(i) }
end

Detect

Here’s where it gets tricky. detect (a.k.a find) returns the first item for which the given block returns true. If no items fit the bill, it returns nil. Easy, right? Just pass nil into each_with_object and set it to an item when applicable:

def detect(&block)
  each_with_object(nil) { |i, obj| obj ||= block.call(i) ? i : nil }
end

I expected that to work, but sadly it does not. It turns out that you can’t mutate nil using each_with_object. That actually makes sense, because you can’t normally assign to nil, but it sure isn’t very useful. No matter what you do inside the block, it just returns nil.

So for this particular method, I reverted to inject, which does allow you to pass nil into it and come out with something else:

def detect(&block)
  inject(nil) { |obj, i| obj ||= block.call(i) ? i : nil; obj }
end

So that works, but it feels a bit like a fail. If you can think of a way to implement detect using each_with_object, please do let me know.

Takeaways

Putting yourself through these little challenges — no matter how silly they seem — is a great way to level up your skills. This was fun and I learned a little something along the way.

Plus, if I ever want to work for/with Josh, I’ll be ready for at least one of his questions ;)

(source code)