Hey: Ruby is expressive. Super expressive. There are many methods which roll up repetitive tasks right there in the core classes. Let’s examine two some.
Enumerable#select (and #detect)
Say I have an array of ActiveRecord objects and I want to find all the objects for which book_type is Comic (as opposed to Novel). And I only want to call AR’s find once.
books = Book.find(:all, :order => 'id desc', :limit => 20) comics = [] books.each do |book| comics << book if book.item_type == 'Comic' end
Okay, that’s one way. I could also use inject. Or, I could just use select:
books = Book.find(:all, :order => 'id desc', :limit => 20) comics = books.select { |i| i.item_type == 'Comic' }
Basically, select will gather any objects for which the block you pass it evaluates to true.
Very calm, very collected.
Use detect if you only want the first match:
books = Book.find(:all, :order => 'id desc', :limit => 20) first_comic = books.detect { |i| i.item_type == 'Comic' }
Enumerable#select (and #detect) sometimes lies
Here’s the catch: select doesn’t work so well on hashes. Check this out:
>> blogs = { :nubyonrails => true, :err => false, :slash7 => true } => {:err=>false, :nubyonrails=>true, :slash7=>true} >> blogs.select { |blog, worth_reading| worth_reading } => [[:nubyonrails, true], [:slash7, true]]
See? We got back an array of arrays when what we wanted was a hash. That complicates things a bit. One soluton is to use Hash[], as mentioned previously in the Hash Fun edition of Err.
>> keepers = blogs.select { |blog, worth_reading| worth_reading } => [[:nubyonrails, true], [:slash7, true]] >> Hash[*keepers.flatten] => {:nubyonrails=>true, :slash7=>true}
That’s kind of weak, though. Hard to remember and ‘twill break if you have nested arrays. So let’s add onto Hash a nested array safe version:
class Array def to_hash Hash[*inject([]) { |array, (key, value)| array + [key, value] }] end alias :to_h :to_hash end class Hash def select(&block) super(&block).to_hash end end
And now:
>> keepers = blogs.select { |blog, worth_reading| worth_reading } => {:nubyonrails=>true, :slash7=>true}
Too much magic? Way too much. I don’t really like overriding core methods. Sure, we could use hash_select or something, but there must be a simpler solution?
There is. Just double negate. Namely, Enumerable#reject.
Enumerable#reject
For some reason the reject method works just like you’d want it to (with hashes). You pass it a block and it returns all the elements which evaluate to false. That is, it rejects any elements for which the block evaluates to true. Using our previous example, let’s play:
>> blogs = { :nubyonrails => true, :err => false, :slash7 => true } => {:err=>false, :nubyonrails=>true, :slash7=>true} >> keepers = blogs.reject { |blog, worth_reading| not worth_reading } => {:nubyonrails=>true, :slash7=>true}
No tricky metaprogramming. No overriding default methods. reject just works. It’s almost the opposite of select, with a little bit more class.
The Real World
Got a range of numbers and only want to find the even ones? Easy, with select:
>> (0..10).select { |n| n % 2 == 0 } => [0, 2, 4, 6, 8, 10]
Have a bunch of files/directories and only want to find files, not directories?
>> Dir['*/*'].reject { |f| File.directory? f } => ["pedalists/Rakefile", "pedalists/README"]
In some of my code I keep a list of recent stories and display them in a sidebar. I don’t want to display the story you’re viewing in the sidebar, though. Hey, reject! (which modifies the receiver in place):
recent_stories.reject! { |i| i.id == @story.id } if @story
That’s it.
The Enumerable mixin contains much goodness we hardly ever talk about. Check it out. Just, tread lightly.
Are you making a comic book app? I registered comiclog.com when I started with rails and went through about four major iterations (the rails framework went through some huge changes every week back then!), but I haven’t picked it back up since…
No comic book app here, I just have ComicVine on the brain.
I’m curious if there is any benefits [or any downsides, for that matter] to only hitting ActiveRecord#find once then processing the results. I’ve found myself doing that recently because sadly [or proudly] my Ruby skills have outgrown my SQL skills. Is it better/faster to process the records in Ruby than, say, MySQL?
Wow – comicvine looks really cool
RSL: If you have the records selected already, go ahead and use ruby to query them further. But, using your database to filter hundreds/thousands/millions of rows will be much faster than ruby.
As always, stay agile. Get it working first, and then get it working well. Prototyping in basic ruby while your app in development is great while things are changing. Once things have been figured out, build some slick db finders to handle the heavy lifting.
Extra Credit: add enumerable support to ActiveRecord. Article.select { |a| a.published? }.sort_by { |a| a.title }. Might be a fun project :)
I can’t remember what the exact scenario was but you kittens might want to be aware that collections of ActiveRecord objects respond to Enumerable#find in an odd way. Using Enumerable#detect solves the problem. [I changed my code to use Enumerable#any? which can also do the trick sometimes.]
I realize this is a somewhat old post, but I just thought of this…couldn’t the following line:
(0..10).select { |n| n % 2 == 0 }
be more cleanly rewritten as:
(0..10).select(&:even?)
Same thing, but takes advantage of the even? method and Symbol#to_proc. I thought it was a bit more intuitive.
Chime in.