Ruby supports two forms of methods:
- Bound Methods: These methods are associated to an object.
- Unbound Methods: These methods are not associated to any particular object.
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
.