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?
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.
???
I never saw this kind of education anywhere else