Ruby 2.7 adds UnboundMethod#bind_call method


Ruby supports two forms of methods:

Let’s say we have a User class.

# user.rb
class User
  def initialize(name)
    @name = name
  end

  def name
    @name
  end

  def welcome
    "Hi, #{self.name}"
  end
end
> user = User.new('John Doe')
# Bound Method
> user.method(:welcome)
#=> #<Method: User#welcome (SOME_PATH/user.rb):6>
# Unbound Method
> User.instance_method(:welcome)
#=> #<UnboundMethod: User#welcome (SOME_PATH/user.rb):6>

We can create UnboundMethod using Module#instance_method or Method#unbind and can call after binding to an object.

What is binding?

A Binding object contains the execution context in the code. The execution context consists of variables, methods, the value of self and block. This context can be later accessed using binding function.

For example:

# person.rb
class Person
  def initialize(name)
    @name = name
  end

  def get_binding
    binding
  end
end

Now try to access name from the instance of Person class.

> person = Person.new('John Doe')
#<Person:0x00007fac42439fe0 @name="John Doe">
> person.name
#=> NoMethodError (undefined method 'name' for #<Person:0x00007fac42439fe0 @name="John Doe">)

We can access the name instance variable using eval. This method takes the code as the first argument and binding as the second argument.

> eval('@name', person.get_binding)
#=> "John Doe"
> person.get_binding.receiver
#<Person:0x00007fac42439fe0 @name="John Doe">

We use Method#bind to bind an object to UnboundMethod. But the allocation from bind is quite expensive.

That is why Ruby 2.7 has added UnboundMethod#bind_call to avoid the intermediate allocation.

Let’s say we have a Manager class which is inherited from User class.

class Manager < User
  def welcome
    "Hi Manager, #{self.name}"
  end
end

To call UnboundMethod:

Before Ruby 2.7

> manager = Manager.new('John Doe')
# Calling an unbound method
> User.instance_method(:welcome).bind(manager).call
#=> "Hi, John Doe"

Ruby 2.7

> manager = Manager.new('John Doe')
# Calling an unbound method
> User.instance_method(:welcome).bind_call(manager)
#=> "Hi, John Doe"

Apart from object, we can also pass parameters to bind_call similar to call method.

class User
  def welcome(name)
    "Hi, #{name}"
  end
end

> user = User.new
> User.instance_method(:welcome).bind_call(user, 'John Doe')
#=> "Hi, John Doe"

Following are benchmark results:

# Student class
class Student   
  def score   
  end
end

> student = Student.new
> N = 100000
> Benchmark.bmbm do |x|
>   x.report("bind.call") { N.times { Student.instance_method(:score).bind(student).call }}
>   x.report("bind_call") { N.times { Student.instance_method(:score).bind_call(student) }}
> end
#
# Rehearsal ---------------------------------------------------
# bind.call   0.066435   0.000766   0.067201 (  0.067263)
# bind_call   0.042495   0.000049   0.042544 (  0.042587)
# ------------------------------------------ total: 0.109745sec
#
#                 user     system      total        real
# bind.call   0.063620   0.000201   0.063821 (  0.063870)
# bind_call   0.040765   0.000025   0.040790 (  0.040812)

As we can see that bind_call is a bit faster than bind.call.