Ruby #call method
ArticleIn Ruby programming Language there is well know feature called Lambda
(and Proc), that acts as an executable chunk of code that can be
passed and may be executed outside of its definition via #call
my_lambda = ->(name, phone){ puts "Hi #{name} your phone-number is #{phone}" }
my_lambda.call('Tomas', '555-012-123')
# Hi Tomas your phone-number is 555-012-123
Now the true value of Lambdas is that they can get passed around as command objects without polluting your business object with logic that is not its concern. For example if you have to print/log something from within an object yet you don’t see a reason why that object should be responsible for holding print/log logic implementation:
class User
attr_accessor :name
attr_reader :contacts, :logger
def initialize(logger: ->(n, p) { } )
@contacts = []
@logger = logger
end
def add_contact(phone)
logger.call(name, phone)
contacts << phone
end
end
user_without_log = User.new
user_without_log.name = "Zdenka"
user_without_log.add_contact("555-012-123")
# no output to log
my_custom_logger = ->(name, phone){ puts "User #{name} added phone number #{phone} to his/her profile" }
user_with_log = User.new(logger: my_custom_logger)
user_with_log.name = "Zdenka"
user_with_log.add_contact("555-012-123")
# User Zdenka added phone number 555-012-123 to his/her profile
user_without_log.contacts
# => ["555-012-123"]
user_with_log.contacts
# => ["555-012-123"]
This code is also really easy to test thanks to lambda:
require `spec_helper`
RSpec.describe User do
# ...
describe 'logging' do
it do
@was_called_with = nil
user = User.new(logger: ->(n, p) { @was_called_with = {name: n, phone: p })
user.name = 'Foo'
user.add_contact(123)
expect(@was_called_with).to eq({name: 'Foo', phone: 123})
end
end
end
But this article is not about lambdas but about #call
method. So let
me show you something else
Imagine that your logger is in a different object (e.g. MyFramework#log
method)
class MyFramework
def log(*args)
puts("Logger received: #{args.join(', ')}")
end
end
my_framework = MyFramework.new
my_framework.log('Hello World', 'whatever', 'foo')
# Logger received: Hello World, whatever, foo
How would you pass it to logger ?
Well most simplest solution would be just create new lambda right?
my_framework = MyFramework.new
user_with_framework_logger = User.new(logger: ->(*args){ my_framework.log(*args)})
user_with_framework_logger.name = "Ruby"
user_with_framework_logger.add_contact("555-012-123")
# Logger received: Ruby, 555-012-123
Wow ! That’s an ugly code
Ruby has a way how to convert methods to Method objects with method and more secure public_method. This method object can be then passed and called within other object:
my_framework = MyFramework.new
method_logger = my_framework.public_method(:log)
method_logger.call('Escape', 'the', 'faith')
# Logger received: Escape, the, faith
user_with_framework_logger = User.new(logger: method_logger)
user_with_framework_logger.name = "Oli Sykes"
user_with_framework_logger.add_contact("555-012-123")
# Logger received: Oli Sykes, 555-012-123
Same apply to class methods
module MyClassMethodBasedFramework
def self.log(*args)
puts("Derp received: #{args.join(', ')}")
end
end
my_logger = MyClassMethodBasedFramework.public_method(:log)
my_logger.call('August', 'burns', 'red')
# Derp received: August, burns, red
user_with_framework_logger = User.new(logger: my_logger)
user_with_framework_logger.name = "Atreyu"
user_with_framework_logger.add_contact("555-012-123")
# Derp received: Atreyu, 555-012-123
I bet there was a time when some senior Ruby dude was trying to sell you on Ruby with sentence: “Ruby is awesome because everything is an Object”. Yes, he was probably showing you that a String is an object not just a type. But literally in Ruby nearly everything is an object! Methods are objects, Class is an object, …think about it.
Now imagine that this generic logger is to simple and you want to pass a
custom object. Well all you need to do is ensure the object
contains common interface method #call
class MyComplexCustomLogger
attr_reader :program_name
def initialize
@program_name = $PROGRAM_NAME # Ruby built in var
end
def call(name, phone)
puts "#{program_name} has logged: User #{name} added #{phone}"
end
end
custom_logger = MyComplexCustomLogger.new
custom_logger.call('Charlie', '555-1234')
# irb has logged: User Charlie added 555-1234
user_with_custom_logger = User.new(logger: custom_logger)
user_with_custom_logger.name = "Helia"
user_with_custom_logger.add_contact("555-012-123")
# irb has logged: User Helia added 555-012-123
So I hope I showed you something new and cool about Ruby. But the point
of the article is to highlight the iportance of #call
Lot of time Ruby developers write single responsibility classes/objects
with single run method named #run
or #execute
:
Person = Struct.new(:year)
tomas = Person.new
tomas.year = 1988
class TellMeYourAge
def initialize(person)
@person = person
end
def calculate_age
Time.now.year - @person.year
end
end
TellMeYourAge.new(tomas).calculate_age
# => 29
class RemoveOldDevelopersFromDB
def initialize(list)
@list = list
end
def run
@list.delete_if { |x| x.year < 1990 }
end
end
list = [tomas]
RemoveOldDevelopersFromDB.new(list).run
list
# => []
And I get it there are cases when you want to describe the object
behavior with the name of interface method. RemoveOldDevelopersFromDB
clearly does execution of command to remove items from DB. Therefore
#run
method kinda make sense.
You can learn about Command Query Separation and why it’s important here
The name of the single responsibility class is usually descriptive
enough so honestly the object would not lose this “description” if we just named the common interface
method #call
:
class TellMeYourAge
def initialize(person)
@person = person
end
def call
Time.now.year - @person.year
end
end
TellMeYourAge.new(tomas).call
# => 29
class RemoveOldDevelopersFromDB
def initialize(list)
@list = list
end
def call
@list.delete_if { |x| x.year < 1990 }
end
end
list = [tomas]
RemoveOldDevelopersFromDB.new(list).call
list
# => []
Plus this way we can pass our objects to other objects with more common protocol:
Puppy = Struct.new(:age)
max = Puppy.new
max.age = 3
everyone = []
everyone << TellMeYourAge.new(tomas)
everyone << max.public_method(:age)
everyone.map(&:call).sum
#=> 32
Call is everywhere in Ruby.
:age.to_proc.call(max)
# => 3
And it’s considered a common interface method name for small single method objects.
Maybe you are writing an object that has now just one method (e.g.: Account#add
) and you know
that it will grow into more method object (e.g.: Account#remove
).
Fine, that’s a good argument to use #add
Maybe that’s not the case but there is still a really good reason to name that method #run
or
#execute
or #calculate
or #fetch
then fine go for it. But be
honest with yourself and your teammates and speak the name of the class and the method name out load before you commit your code
(just to see if it’s really the case).
But if you literally named it this
way just because nothing else popped to your mind the please name the
method #call
This article is heavily inspired by Avdi Grimm’s Ruby Tapas episode callable. I recommend to check it out for further information.
Entire blog website and all the articles can be forked from this Github Repo