Learn about Ruby's send method with a practical example
The send
method is one of the most commonly used Ruby methods when implementing metaprogramming solutions to problems in Ruby. For this reason, it is imperative that you understand how it works and the context in which its a method of choice. There is no better way to do this than with a practical example.
Keep the code below in mind as everything that follows in this post will refer to it.
You have a document.rb
file. This file will contain the metaprogramming code which serves the sole purpose of dynamically building a set of methods ending in the _attributes
suffix with one example being the index_attributes
method in the Post class.
class Document
attr_accessor :view
VIEW_ATTRIBUTES_MAPPING = {
index: [:index_attributes, :display_attributes],
show: [:show_attributes, :display_attributes],
edit: [:edit_attributes, :form_attributes],
new: [:new_attributes, :form_attributes],
create: [:new_attributes, :form_attributes],
update: [:edit_attributes, :form_attributes]
}
def fetch_attributes
possible_attributes_for_view = VIEW_ATTRIBUTES_MAPPING[view.to_sym]
possible_attributes_for_view&.each do |method_for_view|
return send(method_for_view) if respond_to?(method_for_view)
end
end
end
Then a post.rb
file which inherits from what is defined in document.rb
. In this file, we determine a method named index_attributes
which returns an array of items when called.
class Post < Document
attr_accessor :view
def initialize(view)
@view = view
end
def index_attributes
[title, body, cover_photo, creator]
end
end
At some point in the base.rb file, we have a call to the send
method. By examining this line of code, we’ll be able to answer questions about how the send
method works.
- What is the receiver object when calling the
send
method in this line of code? - How does the class of this receiver object change in various contexts?
- What purpose does the
send
method perform in this code and why is it referred to as a metaprogramming method? - The
send
method in this code is used in conjunction with therespond_to?
method. Why is this?
Send has been summoned but by who?
Usually a Ruby method is called like below:
receiver.method
The receiver
being the object which the method
is called on. However, the line of code with the send
method looks peculiar in the sense that, we see a method named send
being called but by who? Who is calling the send
method? The line with the send
method call doesn’t specify an explicit receiver for the method call.
return send(method_for_view) if respond_to?(method_for_view)
If you’re an experienced Ruby developer, its probably quick for you to know that the above line of code is synonymous with the line of code below with the self
being usually ommitted for stylistic reasons.
return self.send(method_for_view) if self.respond_to?(method_for_view)
.
Now that we’ve established that self
is the receiver for the send
method call, all is starting to make sense. However, we are still left with one question to answer. What does self
refer to in this context?
Let’s refresh our minds by going back to what this line of code does. The line is supposed to check if a bunch of methods ending in the _attributes
suffix are defined some where, possibly a Ruby class of some sort. If these methods are defined in that specific place i.e respond_to?
returns true, the defined methods ending in the _attributes
suffix will be called on an object created by that specific class where the method was defined in the first place.
In the post.rb
file, we have an index_attributes
method. If we replace method_for_view
with index_attributes
, the line of code with the send method call will be equivalent to return self.send(:index_attributes) if self.respond_to?(:index_attributes)
.
In English, the line of code above loosely translates to “Call the method named index_attributes
on an object created by a class where the index_attributes
method is defined but, first perform a check that the index_attributes
method is actually defined in this class. Only if its defined, then call the method.”
This highlights two things:
-
The call to
respond_to?
happens first. This checks if theindex_attributes
is defined in the class. Therefore, its safe to say that the value ofself
inself.respond_to?(:index_attributes)
is an instance object whose mother class is the class where theindex_attributes
method is defined. -
The call to
send
happens last. This actually calls the “already defined” method on the specific instance object. Therefore, even in this caseself
is equivalent to an instance object created from the class where theindex_attributes
method is defined.
In both cases, since the index_attributes
method is defined in the Post class, the object which is self
will translate to an instance object of the Post class. By this logic, we expect the method to return an array of the following items; [title, body, cover_photo, creator]
. Let’s try this out in an irb session.
Temporarily modify the fetch_attributes
method to point out the current value of self
.
def fetch_attributes
possible_attributes_for_view = VIEW_ATTRIBUTES_MAPPING[view.to_sym]
possible_attributes_for_view&.each do |method_for_view|
puts "Current value of self is: #{self}"
return self.send(method_for_view) if self.respond_to?(method_for_view)
end
end
Create a new post instance and proceed to call the fetch_attributes
method on it.
3.2.6 :146 > new_post = Post.new(:index)
=> #<Post:0x0000776499e39bb0 @view=:index>
3.2.6 :147 > new_post.fetch_attributes
The current value of self is: #<Post:0x0000776499e39bb0>
=> [:title, :body, :cover_photo, :creator]
3.2.6 :148 >
As you can see, the current value of self
is the newly created instance object named post.
Also, when we call post.fetch_attributes
, we get the array of items returned to us. This is because since we has set the current view to be index, 2 methods related to the index view will be created namely the index_attributes
and display_attributes
. The line return send(method_for_view) if respond_to?(method_for_view)
will check for the index_attributes
method first and if it was defined inside the Post class. Since this is the case, the method will return the output of calling the index_attributes
method ([:title, :body, :cover_photo, :creator] ) and thereafter return.
Now that we’ve established who summoned the send
method, we’ll proceed to investigate the changing nature of self.
Any object can be self depending on context
Inside the Post class, if you look closely, you’ll notice a subtle detail. While send(method_for_view)
can be written as self.send(method_for_view)
, the method in the Post class is not written as below.
class Post < Document
def self.index_attributes
[title, body, cover_photo, creator]
end
end
Why is this the case? To figure this out, lets create a new instance of the Post class and proceed to call the index_attributes
method on it. This results into an error. Why is this?
3.2.6 :222 > post = Post.new(:index)
=> #<Post:0x0000776499f7bb68 @view=:index>
3.2.6 :223 > post.index_attributes
(irb):223:in `<main>': undefined method `index_attributes' for #<Post:0x0000776499f7bb68 @view=:index> (NoMethodError)
post.index_attributes
^^^^^^^^^^^^^^^^^
from /usr/share/rvm/gems/ruby-3.2.6/gems/irb-1.15.2/exe/irb:9:in `<top (required)>'
from /usr/share/rvm/gems/ruby-3.2.6/bin/irb:25:in `load'
from /usr/share/rvm/gems/ruby-3.2.6/bin/irb:25:in `<main>'
from /usr/share/rvm/gems/ruby-3.2.6/bin/ruby_executable_hooks:22:in `eval'
from /usr/share/rvm/gems/ruby-3.2.6/bin/ruby_executable_hooks:22:in `<main>'
3.2.6 :224 >
This is because defining the index_attributes
method as self.index_attributes
changes it from being an instance method to a class method. What this means is that we can nolonger call it on instances of the Post class but rather only on the Class itself. As you can see below, calling the method on the class itself doesn’t throw an error.
Also, its important to keep in mind that when the metaprogramming code is generating methods ending in the _attributes
, the methods for example the index_attributes
method will be generated as index_attributes
instead of self.index_attributes
.
With this, we can conclude, self
can stand for anything. From the class itself to an instance of a class.
Is send really that important? Where would we be without send?
Without the send
method, the fetch_attributes
would potentially look like below.
def fetch_attributes
return index_attributes if respond_to?(index_attributes)
return display_attributes if respond_to?(display_attributes)
return show_attributes if respond_to?(show_attributes)
return edit_attributes if respond_to?(edit_attributes)
return form_attributes if respond_to?(form_attributes)
return new_attributes if respond_to?(new_attributes)
end
As you can see, we’d have to manually write return statements to check if each and everyone of the possible methods is defined.This gets tiring pretty fast, especially if you’re dealing with a lot of methods. With send in our toolbox, we can dynamically generate and call the currently defined method(s) on whatever self is. Put simply, send
allows us to write maintainable code while being lazy along the way.
My name is send and I wanna tell you about my colleague respond_to?
The respond_to?
method is what checks if a method is defined in the class. Without it, the output below is what we’d run into.
A subtle requirement for using send method
Let’s modify the code a bit to identify the value of method_for_view
.
def fetch_attributes
possible_attributes_for_view = VIEW_ATTRIBUTES_MAPPING[view.to_sym]
possible_attributes_for_view&.each do |method_for_view|
puts "Value of method_for_view is: #{method_for_view}"
puts " Class of method_for_view: #{ method_for_view.class}"
return send(method_for_view) if respond_to?(method_for_view)
end
end
As you can see, method_for_view
is a Symbol so, inside the parentheses, when calling the send
method, we pass in a symbol and not a method name.
3.2.6 :314 > post.fetch_attributes
Value of method_for_view is: index_attributes
Class of method_for_view is: Symbol
Value of method_for_view is: display_attributes
Class of method_for_view is: Symbol
=> [:index_attributes, :display_attributes]
3.2.6 :315 >
This reveals a subtle detail about the send
method in Ruby which is that allows 2 options for passing an already defined method to send inside parentheses with one of them being as a symbol(like the current code) and the other as a string.
To further confirm this, let’s perform a simple experiment in an irb
session. The aim of this experiment is pretty simple: to capitalize a string using the upcase
method which ships with Ruby by default.
# create a variable name and assign it to a string
3.2.6 :001 > name = "james"
=> "james"
# call the Ruby method upcase to capitalize the string
3.2.6 :002 > name.upcase
=> "JAMES"
# To capitalize the string using the send and upcase methods
3.2.6 :003 > name.send(:upcase)
=> "JAMES"
3.2.6 :004 > name.send("upcase")
=> "JAMES"
3.2.6 :005 > name.send(upcase)
(irb):5:in `<main>': undefined local variable or method `upcase' for main:Object (NameError)
name.send(upcase)
^^^^^^
Did you mean? case
from /usr/share/rvm/gems/ruby-3.2.6/gems/irb-1.15.2/exe/irb:9:in `<top (required)>'
from /usr/share/rvm/gems/ruby-3.2.6/bin/irb:25:in `load'
from /usr/share/rvm/gems/ruby-3.2.6/bin/irb:25:in `<main>'
from /usr/share/rvm/gems/ruby-3.2.6/bin/ruby_executable_hooks:22:in `eval'
from /usr/share/rvm/gems/ruby-3.2.6/bin/ruby_executable_hooks:22:in `<main>'
3.2.6 :006 >
We see that to capitalize the string using a combination of the send and upcase method, we have to pass in the upcase
method to send
as either a string: name.send(:upcase)
or as a symbol: name.send("upcase")
.
When we try to pass in the actual method name: name.send(upcase)
, we get an error which confirms the subtle detail mentioned earlier about the send
method.
And that’s pretty much all I can say about the send
method in ruby. Before I depart however, I’ll leave you with a question. What happens incase we pass a non-existent method to the send
method? Try it out in the browser and see the result for yourself.