Search code examples
rubyirbruby-2.4

Why does defining a method called ! break IRB?


IRB appears to have strange behaviour when defining a method called !.

To reproduce this, enter the following into IRB:

def !
  puts "foo"
end

Upon creating the method, IRB infinitely prints foo:

irb(main):001:0> def !
irb(main):002:1>   puts "foo"
irb(main):003:1> end
foo
foo
foo
...

As far as I know, you can't directly call a method named ! from Ruby syntax; you have to use send instead. Edit: You can invoke ! as a prefix operator; it's just negation: !x

Why does this definition cause IRB to loop infinitely? Does IRB rely on a method named ! for printing its prompt or something similar?

I'm using Ruby 2.4.3 and IRB 0.9.6 on Windows 10.


Solution

  • tl;dr: Overriding ! outside of a class is a very weird thing to do! There are countless ways that you can "break" ruby by doing crazy things like this - so you may find it fun to play around with such strange ideas, but obviously don't do this in important code!

    In ruby, all classes inherit from the top-level base class: BasicObject. This class defines top-level object negation - i.e. Whenever you write

    !foo
    

    this is actually calling a method called ! on your object foo:

    foo.send(:!)
    

    This makes it possible (although it's a very rare thing to do!) to redefine the method on a specific class. For example, when implementing the null object pattern you could do something like this:

    class NullObject
      def !
        true
      end
    end
    
    my_null = NullObject.new
    !!my_null #=> false
    

    (Normally, the only objects that would return false in the above line are nil and false!)

    Now then, back to your example. What you actually did here was define a method called ! on the class Object (and didn't call super to trigger the original method!). In other words, you basically re-defined the response a fundamental method that gets used all over the place internally. Something, somewhere (??) got confused by this bizarre behaviour and failed non-gracefully.

    irb(main):001:0> def !
    irb(main):002:1> puts "foo"
    irb(main):003:1> super # <-- !! This stops it from breaking completely !!
    irb(main):004:1> end
    => :!
    irb(main):005:0* method(:!)
    foo
    foo
    => #<Method: Object#!>
    irb(main):006:0> method(:!).source_location
    foo
    foo
    => ["(irb)", 1]
    irb(main):007:0> method(:!).super_method
    foo
    foo
    => #<Method: BasicObject#!>
    

    Here are some other ways you could re-define methods to cause bizarre behaviour/errors, for example:

    def nil?
      true
    end
    # Will now die in weird ways!
    
    class String
      def ===(other)
        true
      end
    end
    
    "ruby" === "awesome"
    #=> true