Episode #011: Method and Message

Upgrade to download episode video.

Episode Script

Unlike other dynamic programming languages, such as JavaScript and Python, methods are not a first-class object in Ruby. What do I mean by that? I mean that defining a method does not immediately give you a value you can pass around.

mymethod = def hello
             puts "hello, world"
           end
mymethod # => nil

However, we can ask for an object representing a methods. Here's a greeter class; we ask it for an object representing its #hello method.

class Greeter
  def hello
    puts "hello, world"
  end
end

greeter = Greeter.new
m = greeter.method(:hello) # => #<Method: Greeter#hello>  

We can then call this method object just as we would a proc or lambda:

m.call
# >> hello, world

But why should we even have this extra step? Why aren't Ruby methods first-class, anyway?

Editor's note: Classes are first-class objects in Ruby. Methods are not.

The choice not to make methods first-class objects out of the box is fundamentally consistent with Ruby's nature as a purely object-oriented language, in the vein of Smalltalk. To understand why, we need to understand the difference between methods and messages.

Let's say we're writing a tea timer application. The top-level TeaClock class has a timer collaborator, which will handle the actual counting down, and a ui collaborator, which will handle user interaction, including notifying the user of important events like their tea being ready.

class TeaClock
  attr_accessor :timer
  attr_accessor :ui

  #<<TeaClock-initialize-a>>

  #<<TeaClock-init_plugins>>

  #<<TeaClock-start>>
end

The timer role has the responsibility of waiting until the tea is ready and then triggering an alert. But it doesn't need to know or care how that alert is conveyed to the user. There might be different configurations of the app, one which notifies the user on the command line, another which pops up a growl-style desktop message.

In order to completely decouple the “timer finished” event from how it is shown to the user, we decide to simply inject a notifier object into the timer, which it will #call when ready. Here, we have a simple sleep-based timer.

SleepTimer = Struct.new(:minutes, :notifier) do
  def start
    sleep minutes * 60
    notifier.call("Tea is ready!")
  end
end

That takes care of the timer role. Now for the ui role. Here's a really basic UI implementation, which simply uses STDOUT to communicate with the user.

class StdioUi
  def notify(text)
    puts text
  end
end

On initialization, the TeaClock class wires a timer instance to a ui instance. Remember, the timer just expects a call-able object. So TeaClock turns the ui‘s #notify method into a Method object and passes that to the timer.

We also want our TeaClock to be extensible, so once it's done wiring collaborators together, it initializes any user plugins that have been loaded.

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

To initialize plugins, it looks for any constants defined in the Plugin module namespace, and passes itself to their constructors.

def init_plugins
  @plugins = []
  ::Plugins.constants.each do |name|
    @plugins << ::Plugins.const_get(name).new(self)
  end
end

Here's a simple plugin. It dynamically extends the ui object with a module that adds extra behavior to the notify method. The idea behind this plugin is that it will augment the StdioUi by also ringing the system bell when the time is up. For this demo, so that you can see the behavior in the screencast, we'll have it just print out the word “BEEP” instead of actually ringing the bell.

module Plugins
  class Beep    
    def initialize(tea_clock)
      tea_clock.ui.extend(UiWithBeep)
    end

    module UiWithBeep
      def notify(*)
        puts "BEEP!"
        super
      end
    end
  end
end

Finally, back on our TeaClock class, we define a method to start the countdown, which simply delegates to the internal timer object.

def start
  timer.start
end

OK, it's time to put all this together and try it out. We'll create a TeaClock instance and tell it to start timing. So that we don't have to wait too long to see results, we'll pass it a very small unit of time.

t = TeaClock.new(0.01).start

…wait a second. That's not right. Where's the beep?

The problem lies with how we connected the timer with the UI. When we created a method object for the #notify method:

# ...
self.timer = SleepTimer.new(minutes, ui.method(:notify))
# ...

We passed in a handle to the specific implementation of the #notify method at that point in time. And that point in time was before we initialized the plugins!

Now let's change the TeaClock initialization code to simply pass the whole ui object to the timer, instead of a method object.

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

That means we also need to change the SleepTimer class to call #notify on the notifier, instead of #call. Hold up a second. Did you hear what I just did?

I said “call #notify“. When what I really meant is that SleepTimer should send the =#notify= message= to the notifier. This is a great example of why the message/method divide is so hard to tease apart: conventional language around OO programming has become so dominated by the method-oriented terminology of languages like C++ and Java that we often use them interchangeably. I'll talk about the difference more in a moment, but for now just know that when I write “notifier.notify” in Ruby, that means I'm sending the =#notify= message to the =notifier= object.

SleepTimer = Struct.new(:minutes, :notifier) do
  def start
    sleep minutes * 60
    notifier.notify("Tea is ready!")
  end
end
class TeaClock
  attr_accessor :timer
  attr_accessor :ui

  #<<TeaClock-initialize-b>>

  #<<TeaClock-init_plugins>>

  #<<TeaClock-start>>
end

Now let's try running our timer again.

t = TeaClock.new(0.01).start

Ah, that's better.

This code neatly demonstrates the difference between calling a method and sending a message. When our code saved a direct reference to a method and then called it later, it missed out on any changes to that method which occurred between when the reference was saved and when the method was executed. When we changed the code to send the #notify message to the notifer, it got the most up-to-date implementation of that responsibility.

To summarize the difference:

  • A message is a name for a responsibility which an object may have.
  • A method is a named, concrete piece of code that encodes one way a responsibility may be fulfilled. You might say that it is one method by which a message might be implemented.

You can look at messages as an extra layer of indirection on top of methods. When an object receives a message, it gets to decide how to respond to that message. How it responds to a message may change over the course of the object's lifetime. When code passes references to specific methods around, those references are in danger of going stale. It introduces a subtle temporal coupling between collaborating objects.

By sending messages to their collaborators, objects are assured of getting the current, correct behavior no matter what. The temporal coupling becomes a more benign name coupling.

Alan Kay envisioned objects as independent cells, passing messages to each other with no assumptions about how those messages would be handled. Ruby hews to this vision of object-oriented programming. So if you've ever wondered why we don't pass references to methods around more often in Ruby, now you know.