Hi all,
I'm looking for examples of why it's not a good idea to extend base classes in ruby. I need to show some people why it's a weapon to be wielded carefully.
Any horror stories you can share?
Hi all,
I'm looking for examples of why it's not a good idea to extend base classes in ruby. I need to show some people why it's a weapon to be wielded carefully.
Any horror stories you can share?
One obvious pitfall would be name collisions - if two or more packages choose the same name for a method that behaves differently.
The Trifecta of FAIL; or, how to patch Rails 2.0 for Ruby 1.8.7 has an example of Rails (which is a large, well-scrutinized project) causing problems because they monkeypatched String
to add the method chars
.
There was a pretty famous example of monkey-patching going horribly wrong about 2.5 years ago in Rubinius.
The interesting thing about this case is that both the offending code and the victim were highly visible and highly unusual. Usually, the offender is some piece of code written by some PHP script kiddy who got drunk on his 1337 metaprogramming h4X0r skillz. And the failure mode is a simple ArgumentError
exception, because the original method and the monkeypatch have different arity.
However, in this case, the offender was a library in the stdlib (mathn
) and the failure mode was the Rubinius VM completely blowing up.
So, what happened? Well, mathn
monkeypatches the Fixnum
class and changes how Fixnum
arithmetic works. In particular, it changes both the results and the types of several core methods. E.g.:
r = 4/3 # => 1
r.class # => Fixnum
require 'mathn'
r = 4/3 # => (4/3)
r.class # => Rational
The problem is of course that in Rubinius, the entire Ruby compiler, the entire Ruby kernel, large parts of the Ruby core library, some parts of the Rubinius VM and other parts of the Rubinius infrastructure, are all written in Ruby. And of course, all of those use Fixnum
arithmetic all over the place.
The Hash
class is written in Ruby and it uses Fixnum
arithmetic to compute the size of the hash buckets, compute the hash function and so on. Array
is written in Ruby and needs to compute element sizes and array lengths. The FFI library is written in Ruby and needs to compute memory addresses(!) and structure sizes. Many parts of Rubinius assume that they can do some Fixnum
arithmetic and then pass the result to some C function as a pointer or int
.
And since Ruby doesn't support any kind of selector namespacing or class boxing or similar (although something like that is planned for Ruby 2.0), as soon as some random user code requires the mathn
library, all of those pieces just spectacularly explode, because all of a sudden, the result of a Fixnum
operation is no longer a Fixnum
(which is basically identical to a machine int
and can be passed around as such), but a Rational
(which is a full-fledged Ruby object).
Basically, what would happen, is that some code would require 'mathn'
(or you would type that into IRb), and immediately the VM would just die.
The solution, in this case, was the safe math plugin for the compiler: when the compiler detects that it is compiling the kernel or other core parts of Rubinius, it automatically rewrites calls to Fixnum
methods into calls to private immutable copies of those methods. [Note: I think in current versions of Rubinius, the problem is solved in a different way.]