`ActiveSupport::ArrayInquirer` and even more Rails magic

In the previous article, we dived into ActiveSupport::StringInquirer class and superpowers it gives to String objects.  After I had published that article I decided to take another look at the ActiveSupport module and to my surprise, I found something even more interesting, ActiveSupport::ArrayInquirer class1.

If you read the previous article you might have already guessed what ArrayInquirer does. Directly speaking, it gives superpowers to Array objects.

What does ActiveSupport::ArrayInquirer do?

To understand it better, let’s start with a simple array of names:

names = ['Tom', 'Adam', 'Igor']

If I wanted to check if the array contains my name I would use one of the handy methods from Enumerable Ruby module2:

names.include?("Igor")
 => true

According to a comment3 placed directly inside ActiveSupport::ArrayInquirer class:

Wrapping an array in an ArrayInquirer gives a friendlier way to check its string-like contents.

Let’s check if using the class would make finding my name easier:

names_with_superpowers = ActiveSupport::ArrayInquirer.new(names)
 => NameError: uninitialized constant ActiveSupport::ArrayInquirer from (pry):2:in `<main>'

The class has been officially present in Ruby on Rails source code since version 5.0.0, so to make the above code working I had to switch to an up-to-date version of Rails first:

names = ['Tom', 'Adam', 'Igor']
 => ['Tom', 'Adam', 'Igor']
names_with_superpowers = ActiveSupport::ArrayInquirer.new(names)
 => ['Tom', 'Adam', 'Igor']
names_with_superpowers.class
 => ActiveSupport::ArrayInquirer
names_with_superpowers.Igor? 
 => true
names_with_superpowers.Rob?
 => false

And Voila! Similarly to  ActiveSupport::StringInquirer the class adds some useful methods to Array objects using metaprogramming.

To understand it fully let’s analyse its body step-by-step:

module ActiveSupport
  class ArrayInquirer < Array
    def any?(*candidates)
      if candidates.none?
        super
      else
        candidates.any? do |candidate|
          include?(candidate.to_sym) || include?(candidate.to_s)
        end
      end
    end

    private
      
      def respond_to_missing?(name, include_private = false)
        (name[-1] == "?") || super
      end

      def method_missing(name, *args)
        if name[-1] == "?"
          any?(name[0..-2])
        else
          super
        end
      end
  end
end
  1. ArrayInquirer inherits from Array so all its methods are available to ArrayInquirer objects.
  2. Igor? method is not defined in Array class nor in ArrayInquirer, so method_missing is executed as a fallback.
  3. method_missing checks if a method name ends with ?.
  4. In our case, it ends with?, so any? public method is called with Igor as an argument (Igor[0..-2])
  5. Because any? method received an argument else part of if block is executed.
  6. The else block checks if  the array includes the passed argument(s) either as a string or as a symbol.
  7. The ['Tom', 'Adam', 'Igor'] array includes 'Igor' string so names_with_superpowers.Igor? returns true finally.

Behind the scenes, the class uses the same include? method I used initially 🙂

Similarly to String class, Array also defines inquiry method4 which wraps the current array in theArrayInquirer class.

In addition to magic methods like Igor? you can also use the public any? method on ArrayInquirer object to check if at least one of passed arguments is present in an array:

names_with_superpowers.any?("Igor")
 => true
names_with_superpowers.any?("Rob", "Igor")
 => true
names_with_superpowers.any?("Rob", "Bob")
 => false # Neither "Rob", nor "Bob" is present in the array

Summary

Thanks to spending some time on analysing Ruby on Rails source code I found two new classes that I potentially can use.

At the same time, I would like to give you the very same advice I gave in the previous article. Please always do some benchmarking before you decide to use either StringInquirer or ArrayInquirer. They may be slow.

Interestingly enough, Rails uses the ArrayInquirer class only in one place5 so far. request.variant returns ArrayInquirier object, so instead of writing:

request.variant.include?(:phone)

You can write:

request.variant.phone?

Or:

request.variant.any?(:phone)

I do not recommend using any? in this very case, though 🙂

Footnotes

  1. https://github.com/rails/rails/blob/master/activesupport/lib/active_support/array_inquirer.rb
  2. https://ruby-doc.org/core-2.5.3/Enumerable.html
  3. https://github.com/rails/rails/blob/master/activesupport/lib/active_support/array_inquirer.rb#L4
  4. https://api.rubyonrails.org/classes/Array.html#method-i-inquiry
  5. https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L93
 

Igor Springer

I build web apps. From time to time I put my thoughts on paper. I hope that some of them will be valuable for you. To teach is to learn twice.