Digging deeper into Rails scopes

08 Oct 2015

Background

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.'
  end

  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."
  end

  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  
  end  
end  

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

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

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

© 2024 Keith Pitty, all rights reserved.