Unraveling the mystery of the super keyword

Unraveling the mystery of the super keyword

Using parent functionality in Ruby

It seems that the super keyword in Ruby is a straightforward thing; it simply calls the same method from a parent. However, many developers are still not aware of all the features that the super keyword provides.

This article is not a long one, but I provide essential information about the super keyword, including more advanced tips that may surprise you.

All my notes are based on years of hands-on experience at iRonin.IT - a top software development company, where we provide custom software development and IT staff augmentation services for a wide array of technologies.

Brackets versus non-brackets version

I usually ask the following question during the technical interview for the Ruby developers: what is the difference between calling super and super()?

Passing all arguments automatically to the parent

When you call super, you pass all the arguments to the parent implicitly. To better visualize this, let’s consider the following example:

class Parent
  def call(name, email)
    puts "name: #{name}, email: #{email}"
  end
end

class Child < Parent
  def call(name, email)
    super
    puts "child call"
  end
end

By invoking super in the Child#call method, we implicitly pass the arguments name and email to the call method from the Parent class:

Child.new.call('John', 'john@gmail.com')
# => "name: John, email: john@gmail.com"
# => "child call"

Passing the arguments to the parent explicitly

If, in the above example, we would call super() instead of super, we would receive the following error:

ArgumentError: wrong number of arguments (given 0, expected 2)

It happens because calling super() won’t pass any arguments to the parent method (and because the parent method accepts two arguments, we get an ArgumentError error).

Calling the parent’s block

It is also possible to call a block provided by the parent class:

class Parent
  def call
    yield if block_given?
  end
end

class Child < Parent
  def call(name, email)
    super()
    puts "child call"
  end
end

Child.new.call('John', 'john@gmail.com') do
  puts 'Hello world!'
end
# => "Hello world!"
# => "child call"

In the above example, we didn’t allow to pass any arguments to the parent, but invoking the block was still possible. We can block this behavior as well:

class Child < Parent
  def call(name, email)
    super(&nil)
    puts "child call"
  end
end

Child.new.call('John', 'john@gmail.com') do
  puts 'Hello world!'
end
# => "child call"

Using super in modules

When it comes to the modules and super in Ruby, you can create interesting code using the prepend keyword. Prepend simply takes the module and alters the ancestors’ chain for the class where the module was prepended and puts the module in the first place:

module SomeModule; end

class SomeClass
  prepend SomeModule
end

SomeClass.ancestors
# => [SomeModule, SomeClass, Object, Kernel, BasicObject]

As you can see in the above snippet, the SomeModule module was put before the SomeClass class. If you would define the same method in the module and the class, calling super from the module will call the method from SomeClass.

It’s easier to explain it by writing some code. We can create a simple benchmark class that will measure the execution time of a given method:

module ExecutionTimer
  def call
    time = Time.now
    super
  ensure
    result = Time.now - time
    puts "Call executed in #{result} seconds"
  end
end

class Service
  prepend ExecutionTimer

  def call
    sleep(2)
  end
end

I used ensure in the call method from the module to be sure that the time will be calculated even if the parent method call would raise an error.

Let’s give it a try:

Service.new.call
# => Call executed in 2.000332 seconds

With the prepend and super keywords, you can create helpful "wrappers" for your classes to extend a given method's functionality.

Didn't get enough of Ruby?

Check out our free books about Ruby to level up your skills and become a better software developer.