Named Scope: To Lambda or Not To Lambda, That Is The Question

December 18, 2008

One feature that was added to Rails in 2.1 is named scope. This is a really nice way to provide a flexible API for your controllers to query different types of data without resorting to a conditions hash or even worse, SQL. Here's how it works. Say we have a blog with an Article model, which has a boolean field for published. We could get all published articles like this:

Article.all(:conditions => {:published => true})

But with named scope, we can do this:

Article.published.all

This is a small example so it doesn't seem like much, but this really starts to pay of in terms of readability and encapsulation as things get more complicated. To set up the above named scope, we add this to your Article model:

named_scope :published, :conditions => {:published => true}

By doing this, we have abstracted away the implementation of what it means to be published from the controller and into the model. The advantage is that the code in the controller becomes more "intent revealing" and also if the logic for what it means to be published changes, we just change it in one place. For example, if we decide we need to keep track of when an article was published, we could change our published column from a boolean to a published_at datetime column and just change our named scope like this:

named_scope :published, :conditions => ["published_at is not null"]

If you need to further refine our query, we can chain the calls to multiple scopes together. For example, let's say we add this to our model:

named_scope :featured, :conditions => {:featured => true}

Which allows us to do this in our controllers:

Article.published.featured.all

Now let's say we want to create a named scope for all articles published before a given date. Ok, so we just add this to our model:

named_scope :published_before, :conditions => ["published_at < ?", ?????]

Oops. When we are defining the named scope, we don't know what the date will be. So what do we do? That's where lambda comes into play.

Before we do that, WTF is lambda? Lambda is a name for an anonymous function. The name comes from Lisp. The powerful thing about lambdas is that they are stored in an object and can be passed around as a variable and called at a later time. So here's a very simple lambda:

>> double = lambda{|x| x * 2}
=> #<Proc:0x010dc4b4@(irb):1>
>> double.call 21
=> 42    

We create a lambda that takes one argument and multiplies it by two. We can also call a lambda using the [] method. So in our previous example, we could have done double[21]. I prefer call over [], because mentally the first thing I think of when I see those [] is an Array or Hash, so the call is a little more explicit as to what is going on. To each his own, but it's good to know both ways, because you will come across both ways when reading Ruby code written by others.

This is a very powerful feature which Ruby has and many other mainstream languages lack or make it difficult to do. All functional languages that are gaining popularity now, such as Clojure, Erlang, Scala, and Haskell, also make this kind of thing easy to do.

So now that we know what lambda is, we can see how we can use it with named scope. Since we don't know the value of the date we are trying to compare with to do our published_before named scope, we will instead give named scope a lambda that it will use at the time the SQL query is constructed:

named_scope :published_before, lambda{|date| {:conditions => ["published_at < ?", date] } }

So there we go. This is a lambda that takes the data as a parameter and returns a Hash used for the conditions of the SQL query. Now if we need last year's featured articles, we can now do this:

Article.published_before(Time.now.beginning_of_year).featured.all

Posted in Technology | Tags Rails, Ruby

Comments Comments Feed

1. Nice writeup, clear and easy to follow.

# Posted By Brandon on Friday, December 19 2008 at 10:46 AM

2. With named scopes, you can do just Article.published, no need for Article.published.all unless you want to narrow it down further, e.g. Article.published.all(:limit => 2).

# Posted By Henrik N on Friday, December 19 2008 at 2:47 PM

3. Commented before I read the full post. Just wanted to add that I think this post doesn't really make explicit why the lambda is necessary in this case.

You could do
named_scope :published_before, :conditions => ["published_at < ?", Time.now.beginning_of_year]
but the time would be set at define-time, and not be calculated on each run; with a lambda, on the other hand, it's calculated each time it's run.

# Posted By Henrik N on Friday, December 19 2008 at 2:52 PM

4. Very interesting, nice write-up!

# Posted By Frederic Daoud on Friday, December 19 2008 at 2:53 PM

5. @Henrik N

The intent of published_before is to be able to pass in any date, not have it be always the beginning of the year.

But you bring up a good point. Even if you wanted to define a "published_last_year" scope, you might think you don't need lambda, because you don't need any arguments. But as you said, doing it without the lambda would make the value of beginning of the year be calculated only once, thus the value would never change, creating a nasty bug that you wouldn't find until the year after you deploy your app. :)

# Posted By Paul Barry on Friday, December 19 2008 at 4:01 PM

6. That was like poetry Paul. Thanks.

# Posted By Z on Saturday, December 20 2008 at 6:03 PM

7. Thank you for this informative read, I really appreciate sharing this great post. Keep up your work.

# Posted By suna dumankaya on Tuesday, May 12 2009 at 7:20 AM

8. I think this is an excellent article. Anyone got any more info about it?

# Posted By sbs sonuçlar? on Tuesday, May 12 2009 at 7:21 AM

9. it is easy to understand, especially for newbies like me. thanks. sarah james

# Posted By ygs on Friday, August 28 2009 at 5:22 PM

Comments Disabled