Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “Select a Reject”
    – Chris on January 15, 2007

    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.

  • rick, 41 minutes later:

    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…

  • Chris, about 1 hour later:

    No comic book app here, I just have ComicVine on the brain.

  • RSL, about 14 hours later:

    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?

  • Nathan, about 17 hours later:

    Wow – comicvine looks really cool

  • rick, about 17 hours later:

    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 :)

  • RSL, 3 days later:

    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.]

  • manitoba98, 6 months later:

    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.

  • Chris, 6 months later:
    • manitoba98*: As long as you’re using Rails, sure! to_proc and even? aren’t part of standard Ruby as of 1.8.x.
  • Eight people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.