Rails Active Support Array Extract!
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.
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:
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):
If you want to assign these values, you gotta do that dual variable assignment bit:
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:
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.
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:
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! 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:
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.
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