Select a Reject

Tháng Ba 3, 2009


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'

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 = { |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}
>> { |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 = { |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] }]
  alias :to_h :to_hash

class Hash
  def select(&block)

And now:

>> keepers = { |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.


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| 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| == } if @story

That’s it.

The Enumerable mixin contains much goodness we hardly ever talk about. Check it out. Just, tread lightly.


Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập: Logo

Bạn đang bình luận bằng tài khoản Đăng xuất / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Đăng xuất / Thay đổi )

Connecting to %s

%d bloggers like this: