A couple weeks ago I got my standard This Week in Rails newsletter, and saw that someone had added the extract! “Extract Bang” method to the Array class in Rails (Active Support):

The method removes and returns the elements for which the block returns a true value.

numbers = [1, 2, 3, 4]
odd_numbers = numbers.extract!(&:odd?) # => [1, 3]
numbers # => [2, 4]

Arrays are my favorite, so I dove in a bit and learned more than I bargained for.


Extending Ruby’s Array Class in Active Support

First off, I was reminded that we can add our own methods to Ruby’s core classes, and I saw the convention that Rails uses to incorporate these additions. If you write your extract! code located like so:

active_support/core_ext/array/extract.rb

then you must require it in a file located like so:

active_support/core_ext/array.rb

where array.rb has lines looking something like:

require "active_support/core_ext/array/extract"

So you’ve got all these custom method files extending the core Array class, and one file that requires them all. I learned that these extensions, when added, should have supporting tests (I also learned that Rails doesn’t use RSpec for testing!), located at

rails/activesupport/test/core_ext/array/extract_test.rb

I’d like to go deeper, but this overview is a good start to understanding the Rails framework better. Read more about Active Support Core Extensions.


Array Elements Can Already Be Extracted Though!

Ruby’s Enumerable module has had the #partition method going all the way back to Ruby version 1.8.6 (at least… I’m not sure how to go back further than that!). #partition returns two arrays based on an expression that returns true (first element), or false (second element):

[1,2,3,4,5,6].partition { |v| v.even? }  #=> [[2, 4, 6], [1, 3, 5]]

If you want to assign these values, you gotta do that dual variable assignment bit:

evens, odds = [1,2,3,4,5,6].partition { |v| v.even? }
evens #=> [2, 4, 6]
odds  #=> [1, 3, 5]

For any beginners, remember that the Array class includes the Enumerable module, so it can use the #partition method.

The problem with this approach is that it’s cumbersome. We want to extract elements, not partition-then-assign them! And later we’ll show how Bogdan ensured his Array#extract! method was better than the Enumerable#partition method.

How to Extract!

Here’s the method in all its 6-line glory:

def extract!
  return to_enum(:extract!) { size } unless block_given?

  extracted_elements = []

  reject! do |element|
    extracted_elements << element if yield(element)
  end

  extracted_elements
end

The first line return to_enum... ensures that other methods can be chained thereon. I get the idea, but I’m not sure when you’d call [1,2,3].extract!.<another_method>, so I’d need a concrete example to make it stick. I also don’t know why { size } is used. Need to be walked thru that one. Just think of the first line as the tooth pick that holds together the sandwich.

Toothpick Sandwich

The extracted_elements variable is the bread of our method sandwich. It starts as an empty array, and then returns its new self after extraction is complete.

Let’s look at the meat of our method sandwich:

reject! do |element|
  extracted_elements << element if yield(element)
end

When I first looked at this, I thought, “Does the #reject! method interact with my array, or each element from the block?” The syntactic sugar got a bit sweet, so here’s that first line in all its salty glory:

self.reject! do |element|

SELF! self in this case is our array. So if we have [1,2,3,4,5,6].extract!{|n|n.even?}, then self would be [1,2,3,4,5,6].

I know how to shovel (<<) elements into an array, but I was a bit confused about the yield(element) bit. This is where our block comes into play. Imagine if we had a method called #extract_even_numbers!, it might look something like this:

def extract_even_numbers!
  even_numbers = []

  self.reject! do |element|
    even_numbers << element if element.even?
  end

  even_numbers
end

This very specific extraction method for getting even numbers from an array depends on the element.even? part. OK, now put yourself back into the frame of mind for the #extract! method. The yield(element) bit (I almost used the forbidden “simply” term!) looks at the imposing block, holds the door open, and says, “After you!”. Big block steps in, does it’s little evaluation, and for each element that returns true, we shovel said element into the extracted_elements variable.


Is extract! Actually Better than Partition?

Take a look at the benchmark that compares the methods to see which ones are faster!

At our Continuations meeting this week, @AaronLasseigne also suggested trying #with_object to see if we could make this method sandwich an unwich. Here’s the method, which does indeed function appropriately.

def extract!
  return to_enum(:extract!) { size } unless block_given?

  reject!.with_object([]) do |element, extracted_elements|
    extracted_elements << element if yield(element)
  end
end

But when I ran the benchmark similar to Bogdan’s, it turns out that it didn’t run as fast as his. But it was worth a try! The results using #with_object are from Array#extract_v3!

Warming up --------------------------------------
     Array#partition     1.000  i/100ms
   Array#extract_v1!     1.000  i/100ms
   Array#extract_v2!     1.000  i/100ms
   Array#extract_v3!     1.000  i/100ms
Calculating -------------------------------------
     Array#partition      0.807  (± 0.0%) i/s -      4.000  in   5.002523s
   Array#extract_v1!      1.601  (± 0.0%) i/s -      8.000  in   5.073510s
   Array#extract_v2!      1.772  (± 0.0%) i/s -      9.000  in   5.079500s
   Array#extract_v3!      1.516  (± 0.0%) i/s -      8.000  in   5.285609s

Comparison:
   Array#extract_v2!:        1.8 i/s
   Array#extract_v1!:        1.6 i/s - 1.11x  slower
   Array#extract_v3!:        1.5 i/s - 1.17x  slower
     Array#partition:        0.8 i/s - 2.20x  slower