Ruby #call method

In 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.

Published November 28, 2017
Become a Patron!