Module 1, Topic 13
In Progress

Callable

Module Progress
0% Complete

Back in Episode 11 we went from calling a method object, using the #call message, to sending a message called #notify. In this episode we're going to go the opposite direction, and redeem the #call message a little bit.

Consider a Contest class, which has a #choose_winner method. How it chooses a winner is unimportant for this example. After it chooses a winner, it notifies them. Since notification is part of a different role than choosing a winner, it delegates the task to a collaborator object called @notifier.

class Contest
  def choose_winner
    # ...
    @notifier.notify(winning_user, 
                     "You're a lucky winner!")
  end
end

Now supposing, just for the sake of argument, we change the #notify method to #call instead. What does this buy us?

class Contest
  def choose_winner
    # ...
    @notifier.call(winning_user, 
                   "You're a lucky winner!")
  end
end

Well, let's look at how we might initializer the @notifier instance variable. In the class initializer we'd like to have some kind of bare bones default implementation for the notifier. Here's a version that uses #fetch to see if a notifier has been supplied in the options to the new object. If not, it substitutes a simple lambda. The lambda just prints a message to the console.

class Contest
  def initialize(options={}) 
    @notifier = options.fetch(:notifier) {
      ->(user, message) {
        puts "Message to #{user}: #{message}"
      }
    }
  end
  # ...
end

Since lambdas respond to #call, we can make a quick-and-dirty default notifier without going to the trouble of defining a separate class.

Later on in development, we transition to notifying the user by email. We add a new method to the Contest class responsible for sending the notification emails. Then we change our default notifier to be a method object which references the new #notify_user_by_email method.

class Contest
  def initialize(options={}) 
    @notifier = options.fetch(:notifier) {
      ->(user, message) {
        puts "Message to #{user}: #{message}"
      }
    }
  end

  # ...

  def notify_user_by_email(user, message)
    # ...
  end
end

If you remember Episode 11, you may be wondering if this is a good idea, since we are tightly binding the notifier to a specific implementation of the #notify_user_by_email method. In that episode we took a method reference from the public API of one collaborator and passed it into another collaborator. The biggest difference in this case is that the use of the method object is entirely internal to one object. We are setting an internal instance variable to be a reference to an internal method.

def initialize(minutes)
  self.ui = StdioUi.new
  self.timer = SleepTimer.new(minutes, ui.method(:notify))
  init_plugins
end

So far we've talked about how the class sets up defaults for this collaborator. What about the collaborators that other code might pass in?

We decide that in production, we want users to be notified by SMS text message. The User class has a #send_sms method for just this purpose. To configure a Contest object to send text messages to winners, we simply take the :send_sms symbol, convert it to a Proc, and pass that in as the :notifier.

class User
  def send_sms(message)
    puts "Sending SMS: #{message}"
    # ...
  end
end
# ...
c = Contest.new(notifier: :send_sms.to_proc)

How does this work? The Symbol#to_proc method takes a Symbol and converts it into a proc which takes a receiver object and then sends the symbol as a message to that object.

->(receiver, *args) {
  receiver.send(:send_sms, *args)
}

So when the #choose_winner calls the resulting Proc:

@notifier = :send_sms.to_proc
@notifier.call(user, message)

…it is effectively transformed into this code:

user.send(:send_sms, message)

Time goes by, and we decide to make our contest application social-media-enabled. We decide to notify lucky winners via their Facebook accounts instead of SMS. To do this, we need access to the user's Facebook authorization information. We add a new FacebookNotifer class to the system, which is instantiated with the needed auth info. And we give it a #call method which takes user and message arguments.

class FacebookNotifier
  def initialize(auth_info)
    @auth_info = auth_info
  end

  def call(user, message)
    # ...
  end
end
# ...
fb_notifier = FacebookNotifier.new(auth_info)
c = Contest.new(notifier: fb_notifier)

Of course, no class is complete without tests. In testing the Contest class, we find it convenient to pass in a lambda to collect sent notifications:

before do
  # ...
  fake_notifier = ->(user, message) {
    @sent_notifications << [user, message]
  end
  @contest = Contest.new(notifier: fake_notifier)
  # ...
end

At this point we've seen five different kinds of object which respond to the #call message:

  1. A lambda
  2. A method object
  3. A symbol converted to a Proc
  4. A class implementing the #call method explicitly
  5. A test-specific lambda

#call is one of those "standard protocols", like the "shovel" operator or the #each method, that many different kinds of Ruby object implement. As a result of using this common protocol, you might have noticed that we never once touched the implementation of the #choose_winner method, despite drastically changing how winners would be notified.

I think of #call as the standard protocol for objects which are "executable things". You'll see this in some Ruby libraries: for instance, Rack middlewares must respond to #call. When I see code like this, where the objects have a single "main" method which reiterates the name of the object itself, I usually wonder if implementing the #call protocol would be more appropriate.

notifier.notify
runner.run

That's all for now. Happy hacking!

Responses