Category: Software

ActiveRecord isolated, namespaced scopes and methods using instance_exec and ActiveSupport::Concern

Rails concerns are a really good way to provide shared functionalities into your models. For example you can add scopes and then apply them in multiple classes:

module Statistics
  extend ActiveSupport::Concern

  included do
    scope :disabled, -> { where(disabled: true) }
  end
end

class Host
  include Statistics
end

One of the downsides of this solution is a "models pollution". Every model in which we want to include our module, will get all the methods and scopes from that module. With multiple concerns and extensions, it can create issues with methods and scopes overwriting each other. But as always with Ruby, there's a quite nice solution that will allow you to define custom scopes that will be encapsulated in a namespace method. This approach allows to have multiple scopes with the same name that will vary depending on an invocation context:

module Statistics
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { where('created_at >= ?', 1.week.ago) }
  end
end

module Calculable
  extend ActiveSupport::Concern

  included do
    # Some magic here...
    scope :last_week, -> { unscoped.where('created_at >= ?', 1.week.ago) }
  end
end

class Host
  include Statistics
  include Calculable
end

# last_week accessible via statistics namespace
Host.statistics.last_week #=> all the hosts created last week
# last_week not available directly on a host class
Host.last_week #=> This will raise NoMethodError: undefined method last_week
# last_week should be also chainable with other scopes
Host.enabled.statistics.last_week #=> all enabled hosts from last week
# And the last_week method but from the calculable namespace
Host.calculable.last_week

Proxy object and instance_exec as a solution

To obtain such a behavior we can use a proxy object in our concern module:

module Statisticable
  extend ActiveSupport::Concern

  # Class that acts as a simple proxy for all statistical scopes
  # We have it, because we don't want to "pollute" original klass
  # that we extend with this. This way we can define scopes here
  # and then access them via .statistics class method of a class
  class Proxy
    # @param [Symbol] name of a scope or a method
    # @param [Block] block with a scope that will be binded to the class
    def self.scope(name, block)
      define_method(name) do |*args|
        @scope.instance_exec(*args, &block)
      end
    end

    # @param scope [ActiveRecord::Base] active record class for which we want to
    #   create statistics proxy
    def initialize(scope)
      @scope = scope
      # Here we could also use any scope that we want, like:
      # @scope = scope.unscoped
    end

    %i( week month year ).each do |time|
      # @return [Integer] number of elements that were created in a certain time
      scope :"last_#{time}", -> { where('created_at >= ?', 1.send(time).ago) }
    end

    # We can add any standard method as well - but it can't be chained further
    scope :summed, -> { sum(:id) }
  end

  included do
    # @return [Statisticable::Proxy] proxy that provides us with scopes for statistical info
    #   about a given class
    # @example Get statistics proxy for Like class
    #   Like.statistics #=> Statisticable::Proxy instance
    def self.statistics
      @statistics ||= Statisticable::Proxy.new(self)
    end
  end
end

It uses the instance_exec method to execute all the scopes in our base class (@scope) context.

Now in every AR in which we include this Statisticable concern, all its scopes and methods will be available via statistics namespace.

Ruby: Auto-generated dynamic nested blocks wrappings for Ruby and its performance

It's not really often that you don't know what will be in a block. Especially when you plan to nest blocks multiple times. However things like this happen...

Typical nested blocks

Here's a "typical" example of nested blocks:

class TimeBenchmarkWrapper
  def monitor
    time = Time.now
    yield
    ms = (Time.now - time) * 1000
    print "#{TimeBenchmarkWrapper}: Time taken: #{ms}ms\n"
  end
end

class LoggerWrapper
  def monitor
    print "#{LoggerWrapper}: logging something\n"
    yield
  end
end

class AroundFilterWrapper
  def monitor
    return unless before_action
    yield
    after_action
  end

  def before_action
    print "#{AroundFilterWrapper} before logic\n"
  end

  def after_action
    print "#{AroundFilterWrapper} after logic\n"
  end
end

class ExampleClass
  def execute
    # Here should the code go
    print "#{ExampleClass} executing...\n"
  end
end

time = TimeBenchmarkWrapper.new
logger = LoggerWrapper.new
around = AroundFilterWrapper.new

logic = ExampleClass.new

around.monitor do
  logger.monitor do
    time.monitor do
      logic.execute
    end
  end
end

The code above is really simple. It just wraps around a business logic with some monitors (logger, benchmark, around filter). But...

Reordering, rearranging, dynamic number of nested blocks

Everything is awesome until we decide to to have a dynamic order and amount of nested blocks that perform some sort of logic around our business logic.

Let's say that I would like to do something like this:

wrappers = [ClosestWrapper, MiddleWrapper, OuterWrapper]

wrapped_block = wrap_with(wrappers) do
  # The "proper" business logic
  logic.execute
end

In the wrappers array we would like to have all the wrappers that we want to use in a given order. And then the magical method wrap_with should somehow nest all the blocks. So how can we achieve such a behaviour?

Multiple blocks injecting

The solution to this problem is quite simple: defining blocks that accept block parameter and them injecting one into another. If you don't understand the code, please read to comments in the code - they explain all the details:

def wrap_with(wrappers, &block)
  # We define the most bottom block - that should evaluate the real code
  # All the blocks are in array because we will use inject to inject one into another
  # and since we will be injecting first into second, second into third, etc
  # our "base" proc needs to be first (it will be the most inner block)
  blocks = [-> { block.call } ]

  # Each wrapper needs to be wrapped with a proc that accepts a inner block containing
  # stuff that should be inside - this inside stuff is a proc as well and
  # will be executed. That's why when we execute the most outer block if will execute
  # the inner one (and it will happen as a cascade)
  blocks += wrappers.map do |wrapper|
    proc do |inner_block = nil|
      wrapper.new.monitor do
        inner_block.call
      end
    end
  end

  # In general it is equal to a code like this (but generated dynamically):
  # We assume that we have following monitors: [Monitor1, Monitor2, Monitor3]
  # Monitor3.new.monitor do
  #   Monitor2.new.monitor do
  #     Monitor1.new.monitor do
  #       proxy.call(*args, &block)
  #     end
  #   end
  # end
  blocks.inject do |inner, resource|
    proc { resource.call(inner) }
  end.call
end

# Example usage - now we can change order and amount of wrappers
# and everything will still work
wrappers = [TimeBenchmarkWrapper, LoggerWrapper, AroundFilterWrapper]

wrap_with(wrappers) do
  ExampleClass.new.execute
end

If you still don't get it, here's an illustration of how the blocks are injected:

blocks

Performance impact

Below you can see, that from 1 to 100 nested blocks, "automated" nested blocks perform around 2.5-3 times slower than a standard Ruby code. The more nestings you have the slower it gets. The generated nestings are slower mostly because they use more resources for managing (storing, handling) more blocks.

performance_blocksperformance_loss

Conclusion

If the performance is not your primary goal and you prefer visibility and flexibility over it, then the auto-generated block approach is definitely better. However it is still worth keeping in mind, that even with normal blocks, the more of them we have the slower it gets.

Benchmark sources: github.com/mensfeld/benchmarks/tree/master/Ruby-dynamic-nested-blocks

Copyright © 2026 Closer to Code

Theme by Anders NorenUp ↑