Enumerable's *ect Methods Reimplemented Using `each_with_object`
20 May 2012
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 ;)