Array Inquiry in Rails

Array Inquiry in Rails

The ArrayInquirer class provided by the Active Support framework in Rails provides a readable way to check the contents of an array. This post explores how you can implement this using metaprogramming in Ruby.

2 min read

You can check if an array contains a particular element using the include?(obj) method.

browsers = ['chrome', 'safari', 'firefox']

browsers.include?('chrome')   # true
browsers.include?('explorer') # false

This code is already very simple and readable. But, can we go one step further and call a method named chrome? on the array itself?

browsers = ['chrome', 'safari', 'firefox']

browsers.chrome?   # should return true
browsers.explorer? # should return false

With a little bit of metaprogramming in Ruby, we absolutely can.

Extending Array

Let's create a new class that extends Ruby's Array class and override the method_missing method on it. This method is defined on the BasicObject class, which all Ruby classes inherit from, and the Ruby interpreter calls it when it can't find the method on an object.

class ArrayChecker < Array
  def method_missing(name, *args)
    if name.end_with?("?")
      any?(name[0..-2])
    else
      super
    end
  end
end

By default, the interpreter raises an error when this method is called. However, you can override this method to provide more dynamic behavior and handle undefined methods.

class Person
  def method_missing(symbol, *args)
    puts "You called '#{symbol}' method which doesn't exist."
  end
end

ak = Person.new
ak.name  # You called 'name' method which doesn't exist.

The ArrayChecker class overrides the method_missing method and checks if the method that was called ends with ?. If it does, then it calls the any? method on the array, passing the method name without the ?. This method returns true if the array contains the value passed.

For example, if you call browsers.chrome? where browsers is an instance of the ArrayChecker class, the Ruby interpreter will first check if there's a method named chrome? on ArrayChecker.

Since ArrayChecker doesn't include chrome? method, Ruby will call the method_missing, which in turn calls browsers.any?('chrome') , checking if the array includes a value named 'chrome'. If yes, it returns true, returning false otherwise.

This allows us to write the following code.

browsers = [:chrome, :safari, :firefox]

browsers.chrome?    # true
browsers.explorer?  # false

Use ActiveSupport::ArrayInquirer

Luckily, if you're using Ruby on Rails, you don't have to write the above code yourself. The Active Support library provides an ArrayInquirer class which handles it for you. It also supports both string and symbol values.

Wrapping an array in ArrayInquirer provides a friendlier way to check its contents:

browsers = ActiveSupport::ArrayInquirer.new([:chrome, :firefox])

browsers.chrome?    # => true
browsers.firefox?   # => true
browsers.explorer?  # => false

In addition, this class also overrides the any? method, so it checks for both the stringified and symbolized form of any element in the array.

browsers = ActiveSupport::ArrayInquirer.new([:chrome, :firefox])

browsers.any? # true
browsers.any?(:chrome, :firefox) # true
browsers.any?(:explorer, :firefox) # true
browsers.any?(:explorer, :desktop) # false

browsers.any?('chrome') # true
browsers.any?('explorer') # false

You can check the implementation of this class on Github.

The inquiry Helper

In addition, Active Support opens the Array class and adds the inquiry method to it. That way, you don't have to write ActiveSupport::ArrayInquirer.new(array) every time. Instead, you can simply call array.inquiry method to get the same effect.

pets = [:cat, :dog].inquiry

pets.cat?     # => true
pets.horse?   # => false

pets.any?(:cat, :horse)  # => true
pets.any?(:horse, :cow)  # => false

Here's the implementation of the Array#inquiry method. It instantiates the ArrayInquirer class, passing self which is the array itself.

class Array
  def inquiry
    ActiveSupport::ArrayInquirer.new(self)
  end
end

So this concludes our exploration of the ArrayInquirer class. You can use this class as a readable way to check the contents of an array.

Hope you found this post helpful.