any? != ! empty?

Jared Carroll

A common Ruby/Rails idiom used in views looks like the following:

<% if @posts.empty? -%>
  There are no posts.
<% else -%>
  <ul>
    <%= render :partial => 'post', :collection => @posts -%>
  </ul>
<% end -%>

Say that was from /posts or PostsController#index.

Alternatively, I’ve seen this:

<% if @posts.any? -%>
  <ul>
    <%= render :partial => 'post', :collection => @posts -%>
  </ul>
<% else -%>
  There are no posts.
<% end -%>

Testing for the positive first instead of the negative first.

The following is also valid, but its not legal in my book because it’s trash:

<% unless @posts.empty? -%>
  <ul>
    <%= render :partial => 'post', :collection => @posts -%>
  </ul>
<% else -%>
  There are no posts.
<% end -%>

I bet that took a couple takes before you got that. ‘unless … else’ is a terrible Ruby construct.

Sorry got sidetracked on my ‘unless … else’ hate.

Take a look at the following Ruby:

[1, 2].any? => true
[nil, nil].any? => false

[1, 2].empty? => false
[nil, nil].empty? => false

I never liked using #any? without a block, it felt/feels too strange. Now if you don’t pass #any? a block you get a default block, which looks like this:

[1, 2].any? {|each| each}

Now #any? is a method that asks is there anything in this collection that’s true?. In Ruby, the existence of an object is considered true; so calling #any? without a block is saying is there something that’s not nil or false in this collection?.

So the view code from above:

<% if @posts.any? -%>
  <ul>
    <%= render :partial => 'post', :collection => @posts -%>
  </ul>
<% else -%>
  There are no posts.
<% end -%>

Isn’t it really saying is there anything in this posts collection? Because that collection could contain nil. And nil is an object, its an instance of NilClass; so the collection actually does have something in it. Using #any? is this context, basically as way of saying ‘not empty’, is incorrect because it’s relying on Ruby’s ugly feature of ‘object existence implies truth’ to test if there’s anything in a collection.

The correct code, when testing the positive first, should be:

<% if ! @posts.empty? -%>
  <ul>
    <%= render :partial => 'post', :collection => @posts -%>
  </ul>
<% else -%>
  There are no posts.
<% end -%>

About thoughtbot

We've been helping engineering teams deliver exceptional products for over 20 years. Our designers, developers, and product managers work closely with teams to solve your toughest software challenges through collaborative design and development. Learn more about us.