May 26, 2019

Understanding how Ruby initializes objects

Understanding how Ruby initializes objects

Deep buried in Rails source code, there is a code snippet:

class DeprecationProxy
  def self.new(*args, &block)
    object = args.first

    return object unless object
    super
  end
  ...
end

It was added 9 years ago and the commit message said:

Override new on proxy objects so that they never wrap nil or false.

The question is that how does this work? Because we know that when an object is initialized using new, Ruby executes the initialize method.

class Greeting
  def initialize(message)
    puts "Hello there! #{message}"
  end
end

Greeting.new("Welcome to Ruby world.")
=> "Hello there! Welcome to Ruby world."

Notice that we have not actually defined new method anywhere. So how does this code from Rails work?

To see how things work under the hood, let’s introspect where the new method is defined on the class.

Ruby provides a handy method which is named method to see where a method is defined.

>> Greeting.method(:new)
=> #<Method: Class#new>

We can see that the new method actually comes from Class. But then how does the initialize method gets called when we call new on a class?

Turns out, Ruby internally calls initialize on the object passing all the arguments that were passed to new.

greeting = Greeting.new(message)
=> Calls Class.new(message)
=> Calls initialize on the object
=> which results in:
=> greeting.initialize(message)

As Ruby allows us to override any method, we can override the new class method as well.

class Greeting
  def self.new(args)
    puts args
    puts "New ends here."
  end

  def initialize(args)
    puts "I am getting initialized with #{args}"
  end
end

Let’s see what is the output of creating an object of the Greeting class now.

Greeting.new({name: "Prathamesh", country: "India"})

>> Greeting.new({name: "Prathamesh", country: "India"})
{:name=>"Prathamesh", :country=>"India"}
New ends here.
=> nil

As expected, Ruby happily calls the overridden new method and no longer calls the initialize method because we did not call it from our definition of new.

Now let’s look at the snippet from Rails that we discussed earlier:

class DeprecationProxy
  def self.new(*args, &block)
    object = args.first

    return object unless object
    super
  end
  ...
end

It is clear that the new method is overridden by the DeprecationProxy class and it returns same object when the object has a falsey value.

return nil unless nil
return false unless false

It should also be noted that this overridden new method calls super on the last line which in turn calls Class#new. This makes sure that Ruby’s semantics of new method are preserved. This will make sure that DeprecationProxy.newwill call initialize method of DeprecationProxy after the overridden newmethod is executed.

Subscribe to my newsletter to know more about how Ruby works by prying out Rails code.