Digging deeper into Rails scopes

08 Oct 2015


Recently I wrote a post about Rails scopes and lambdas in an attempt to clarify why it is so common to see the use of lambdas with Rails scopes. I now realise that I could have gone a bit further in my explanation.

A question that is sometimes asked is:

Why are lambdas used with Rails scopes? Why not procs or blocks?

To answer that question it is useful to first look at the implementation of the Rails scope method in ActiveRecord::Scoping::Named::ClassMethods.

Implementation Details

The implementation is as follows:

def scope(name, body, &block)  
  unless body.respond_to?(:call)  
    raise ArgumentError, 'The scope body needs to be callable.'

  if dangerous_class_method?(name)  
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but Active Record already defined " \
      "a class method with the same name."

  extension = Module.new(&block) if block

  singleton_class.send(:define_method, name) do \|\*args\|  
    scope = all.scoping { body.call(\*args) }  
    scope = scope.extending(extension) if extension

    scope || all  

Now let’s consider this implementation in conjunction with the following scope:

class Article < ActiveRecord::Base  
  scope :created_before, ->(time) { where("created_at < ?", time) }  

In this example, :created_before is interpreted as name and ->(time) { where("created_at < ?", time) } as body.

Block, Proc or Lambda?

Notice that body must be callable. So that rules out a block, which is simply a syntactic structure. However, it does still allow body to be a lambda or a proc.

Whilst it is technically possible for a proc to be used in conjunction with a Rails scope, a lambda is more useful because of the constraint that it must, unlike a proc, have a specific arity. For example, in the example above, Article.created_before must be called with one argument.

Summing up

Hopefully, that explains why lambdas are used with Rails scopes.

Of course, you’ll notice that the implementation of the scope method uses metaprogramming via :define_method to create a class method that could have been programmed directly.

Other posts

Previous post: Rails scopes and lambdas

More recently: Reflecting on Ruby Conf AU 2016

© 2023 Keith Pitty, all rights reserved.