In Progress
Unit 1, Lesson 1
In Progress

Advanced Class Membership

Video transcript & code

In episode #450, we introduced the topic of how to check whether an object is a member of a given class. We talked about how we can often avoid performing such a check altogether, either by simply duck-typing our code, or by forcing a conversion to the type we want .

We talked about how, if we really do decide to check an object's class, the aliased is_a? or kind_of? methods are usually the ones we want to use.

mysterio[3]                     # => "Quack"

Array(mysterio)                 # => ["Moo", "Woof", "Hiss", "Quack"]
mysterio.to_a                   # => ["Moo", "Woof", "Hiss", "Quack"]
mysterio.to_ary                 # => ["Moo", "Woof", "Hiss", "Quack"]

class MoreAwesomeArray < Array
end

stuff = MoreAwesomeArray.new

stuff.class == Array            # => false
stuff.class == MoreAwesomeArray # => true

stuff.instance_of?(Array)       # => false
stuff.instance_of?(MoreAwesomeArray) # => true

stuff.is_a?(Array)             # => true
stuff.kind_of?(Array)          # => true

So, at this point you might be feeling like you have a good handle on how to test an object's class in Ruby. But I have some bad news for you: All of the class-inclusion testing techniques we've seen so far have a hidden weakness.

Every now and then, you may run into a special object that is inherited from BasicObject instead of from Object.

Let's ask this special object whether its an Array.

class Special < BasicObject
  # ...
end

special = Special.new

special.is_a?(Array) # ~> NoMethodError: undefined method `is_a?' for #<Special:0x00561df19ddc98>
# =>

# ~> NoMethodError
# ~> undefined method `is_a?' for #<Special:0x00561df19ddc98>
# ~>
# ~> xmptmp-in1219573x.rb:7:in `<main>'

Whoah. Instead of a straight yes-or-no answer, it turns out the object doesn't even understand the question.

OK, what about asking it if it's an instance_of? Array?

special.instance_of?(Array) # ~> NoMethodError: undefined method `instance_of?' for #<Special:0x005605498161c8>

Nope, that doesn't work either.

OK, but surely we can manually check to see the object's class is equal to a certain value, right?

special.class == Array # ~> NoMethodError: undefined method `class' for #<Special:0x00558795049b98>

Even this doesn't work! The special object doesn't even understand the message class.

This is a great illustration of Ruby's commitment to making everything an object, and nearly every operation a message sent to an object. In a lot of languages, class wouldn't be a "real" method; it would just be special language syntax that masqueraded as a method.

But here we can see that Ruby treats .class just like any other attribute. The whole point of BasicObject is to have a base class that supports only an ultraminimal set of messages, and class didn't make the cut.

So now we've seen that a potential drawback of all the class-inclusion tests we've learned so far is that in certain rare cases, they might raise a NoMethodError exception instead of resulting in a true or false answer.

But that's not the only danger. We also have to worry about outright dishonesty.

To illustrate what I mean by this, let's welcome our old friend ActiveRecord onto the stage.

This is an older version of the ActiveRecord library, but one that's still in fairly widespread use.

Let's create a new Mermaid record, and start her out with a nice assortment of widgets.

Now let's ask to see her collection.

This collection appears to be an array.

Just to be sure, though, let's compare its class to the Array class.

Yep, it's an Array. But just to be doubly sure, let's ask it flat out whether it is an Array.

Everything looks consistent here. So, case closed, right? This collection of widgets is an array.

gem "activerecord", "~> 3.0"
require "active_record"
require "sqlite3"

class ActiveRecord::Base
  establish_connection adapter:  "sqlite3",
                       database: ":memory:"

  connection.create_table( "mermaids" ) do |t|
    t.string :name
  end

  connection.create_table( "widgets" ) do |t|
    t.string :name
    t.integer :mermaid_id
  end
end

class Mermaid < ActiveRecord::Base
  has_many :widgets
end

class Widget < ActiveRecord::Base
  belongs_to :mermaid
end

ActiveRecord::VERSION::STRING   # => "3.2.22.4"

ariel = Mermaid.create name: "Ariel"
Widget.create name: "gizmo", mermaid: ariel
Widget.create name: "gadget", mermaid: ariel
Widget.create name: "whosit", mermaid: ariel
Widget.create name: "whatsit", mermaid: ariel

ariel.widgets
# => [#<Widget id: 1, name: "gizmo", mermaid_id: 1>,
#     #<Widget id: 2, name: "gadget", mermaid_id: 1>,
#     #<Widget id: 3, name: "whosit", mermaid_id: 1>,
#     #<Widget id: 4, name: "whatsit", mermaid_id: 1>]
ariel.widgets.class == Array    # => true
ariel.widgets.is_a?(Array)      # => true

What if I told you that everything on this screen is a lie?

To shatter the illusion, we need to introduce one final and definitive technique for testing whether an object is a member of a class. Instead of asking the object, let's ask the class.

We do this by using the threequals, or case-equality operator with the class on the left and the object on the right.

Array === []         # => true

When we ask the class whether the widgets collection is an instance, the class replies: "never heard of it!"

Array === ariel.widgets         # => false

So what is this mysterious non-array collection? Well, if ask Ruby for every Class in the system , then filter out Object and BasicObject , then match each remaining class against our mystery object, we finally get the answer that it's an ActiveRecord::Collections::AssociationProxy.

(ObjectSpace.each_object(Class).to_a - [Object, BasicObject]).
  detect{|c| c === ariel.widgets}
# => ActiveRecord::Associations::CollectionProxy

Wow, that was a lot of effort to get at the truth!

Oh, ActiveRecord. You almost had us this time! Let's give a big hand to this master of illusions, as it leaves the stage!

How do you think ActiveRecord pulled off this feat of prestidigitation? The truth is, there's really no magic to it at all. We can easily enough achieve the same effect ourselves.

Here's a class that defines its own class method, its own is_a? method with an alias to kind_of?, its own instance_of? predicate, and its own inspect={{{shot(26)}}} and aliased =to_s method.

When we create an instance of this object and interrogate it, it swears up and down that it's an honest to goodness array.

Only by asking the Array class are we able to discern the truth.

class NotArray
  def class
    Array
  end

  def is_a?(other)
    return true if other <= Array
  end

  alias kind_of? is_a?
  alias instance_of? is_a?

  def inspect
    "[]"
  end

  alias to_s inspect
end

not_array = NotArray.new
# => []

not_array.class == Array        # => true
not_array.is_a?(Array)          # => true
not_array.kind_of?(Array)       # => true
not_array.instance_of?(Array)   # => true

Array === not_array             # => false

What we've just seen really serves to drive home the fact that Ruby is serious about the pure object-oriented philosophy of objects being defined by their behavior alone. Nearly every interaction we have with an object is in terms of messages we send to it, and the object can be defined to answer those messages however it wants! Even when the message is, "what class are you?", the object gets to decide how to answer.

OK, so we've seen that objects may not be able to respond to messages like is_a? and even class. And we've seen that even when they do, they may not be honest.

The question is, when do we need to worry about this? Assuming we have a reason to check object classes at all, how defensively do we need to write our type-testing code?

The answer, as always, is: it depends. But as a broad guideline, here's what I'd suggest:

First, like we said at the beginning, decide if you really, truly need to check an object's type.

Can you get away with simply sending it messages and seeing what happens?

And if not, can you make do with either strictly or leniently converting the object into what you need? Instead of explicitly checking its class?

If you decide you really do need to ask objects what they are, and you're writing application level code, send the is_a? or kind_of? messages. Trust your objects. At the application level, you're probably not going to be dealing with weird objects that are unable to answer these questions, and if you are, it probably indicates a coding mistake that you need to know about.

And when you're working with domain-level objects, any lies they are telling are probably for a good reason.

If, on the other hand, you're writing lower level infrastructure code, then it's time to code more defensively. For instance, you might be writing a new kind of data structure that might be asked to hold any possible kind of object. Or, you could be writing some testing framework code that needs to be robust no matter what kind of bizarro client objects it is asked to work with.

In cases like these, when you know you can't make any assumptions at all about the objects you're dealing with, then it's time to use the class threequals operator. The only way this technique can fail is if the code you're interacting with actually monkey-patches the class threequals operator itself. Otherwise, it's safe and trustworthy.

And that's your guide for testing object class membership in Ruby. Happy hacking!

Responses