Discussion About Open Classes and Monkey Patching at #retroruby
While at RetroRuby 2014 a very good question was asked in the newbie track. The following code sample had just been shown on some basic awesomeness of Ruby:
1 2 |
|
One of the participants then asked why that worked but the inverse didn’t:
1 2 |
|
Oops what just happened?
In Ruby, virtually everything is accomplished by sending a message to another object. Above we would say:
Send the message
*
to"Words"
with parameter2
Send the message
*
to2
with parameter"Words"
“Sending a message” is the Java equivalent of calling a method. Another way to write the above is:
1 2 |
|
Here we used the normal “method” calling syntax: obj.method(args)
You’ll also probably see the following:
1 2 |
|
This time we explicitly sent the message: obj.send(message, args)
With send
Ruby
doesn’t check if the message you passed was supposed to be public or private.
Generally, what you wanted to do was dynamically send the message while still
making sure to respect the public API of the object. To do this you should use
public_send
instead: obj.public_send(message, args)
.
So back to the original issue. Both
String
and
Fixnum
respond to the message
*
.
However, String
’s implementation knows what to do when the argument is a
Fixnum
. When we reverse it, the Fixnum
implementation doesn’t understand
what to do with a String
argument.
How to fix this?
Well you probably shouldn’t. But just for fun we’ll use Ruby’s open class
behavior to monkey patch Fixnum
’s *
implementation.
1 2 3 4 5 6 7 8 9 10 11 |
|
It worked!! Before you go doing this to everything, be aware we’ve now lost the
ability to do normal multiplication. Additionally, we’ll overflow the stack
trying to multiply two Fixnum
s.
1 2 |
|
Wrap Up
In Ruby, most things are messages sent from one object to another. Some times the language gives a little extra syntactic sugar to make sending the message more readable.
Additionally, open classes and monkey patching have their uses. Just be aware that with great power comes great responsibility. Always stop to ask yourself if this is really a good idea; and try to never change existing behavior if you can.
Level Up!
So what if we just wanted to extend the existing functionality. There are a lot of ways to do this and covering each, along with the pros and cons is very out of scope for this post. I’m just going to demonstrate one way to illustrate how it could be done.
You’ll need a new irb or Pry session so we get the original implementation for
Fixnum
back.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|