Search code examples
rubyclassmoduleextend

Why can't I extend the Fixnum class in a module and use it?


I've created a module in which I extend the Fixnum class with a new method. But when I require the module and try to use the extended method, it returns:

NoMethodError: undefined method `roundup' for 13:Fixnum

Here's what my module looks like:

module EanControl
  # Extend Fixnum with #roundup
  class Fixnum
    def self.roundup
      return self if self % 10 == 0   # already a factor of 10
      return self + 10 - (self % 10)  # go to nearest factor 10
    end
  end

  # More code...
end

This is what I'm doing:

require 'path_to_module'
12.roundup

# => NoMethodError: undefined method `roundup' for 13:Fixnum

How could I solve this?


Solution

  • There are three problems with your code:

    1. You are creating a new class EanControl::Fixnum, but you actually want to change the existing builtin ::Fixnum. Solution: explicitly start the constant lookup from the top-level, or, more idiomatically, just drop the module.

      module EanControl
        class ::Fixnum
          # …
        end
      end
      
      # although it would be much simpler to just do this:
      
      class Fixnum
        # …
      end
      
    2. You define roundup as a singleton method of the object Fixnum, but you call it as an instance method of instances of Fixnum. Solution: make roundup an instance method:

      class Fixnum
        def roundup
          return self if (self % 10).zero? # already a factor of 10
          self + 10 - (self % 10)          # go to nearest factor 10
        end
      end
      
    3. The Ruby Language Specification does not actually guarantee that there even is a Fixnum class. It only guarantees that there is an Integer class, and it allows that different implementations may provide implementation-specific subclasses. (E.g. YARV has Fixnum and Bignum subclasses of Integer.) Since you only add the method to Fixnum, it won't work for other Integers, which aren't Fixnums. And since the range of Fixnums is different for different implementations of architectures (e.g. on YARV on 32 bit systems, Fixnums are 31 bit, on 64 bit systems, they are 63 bit, on JRuby, they are always 64 bit), you don't even know for sure what numbers your method will work on and when it will fail. (E.g.: 9223372036854775808.roundup # NoMethodError: undefined method 'roundup' for 9223372036854775808:Bignum.) Solution: make the method an instance method of Integer:

      class Integer
        def roundup
          return self if (self % 10).zero? # already a factor of 10
          self + 10 - (self % 10)          # go to nearest factor 10
        end
      end
      

    Lastly, I want to suggest at least using a mixin here:

    module IntegerWithRoundup
      def roundup
        return self if (self % 10).zero? # already a factor of 10
        self + 10 - (self % 10)          # go to nearest factor 10
      end
    end
    
    class Integer
      include IntegerWithRoundup
    end
    

    Now, if someone else debugs your code, and wonders where this roundup method comes from, there is a clear trace in the ancestry chain:

    12.method(:roundup).owner
    # => IntegerWithRoundup
    

    Even better would be to use a Refinement, that way your monkeypatch doesn't pollute the global namespace:

    module IntegerWithRoundup
      module Roundup
        def roundup
          return self if (self % 10).zero? # already a factor of 10
          self + 10 - (self % 10)          # go to nearest factor 10
        end
      end
    
      refine Integer do
        include Roundup
      end
    end
    
    12.roundup
    # NoMethodError: undefined method `roundup' for 12:Fixnum
    
    using IntegerWithRoundup
    
    12.roundup
    # => 20