39

I want to define an instance method Date#next which returns the next day. So I made a DateExtension module, like this:

module DateExtension
  def next(symb=:day)
    dt = DateTime.now
    {:day   => Date.new(dt.year, dt.month, dt.day + 1),
     :week  => Date.new(dt.year, dt.month, dt.day + 7),
     :month => Date.new(dt.year, dt.month + 1, dt.day),
     :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
  end
end

Using it:

class Date
  include DateExtension
end

Calling the method d.next(:week) makes Ruby throw an error ArgumentError: wrong number of arguments (1 for 0). How can I override the default next method from Date class with the one declared in DateExtension module?

resilva87
  • 3,325
  • 5
  • 32
  • 43

2 Answers2

110

In Ruby 2.0 and later you can use Module#prepend:

class Date
  prepend DateExtension
end

Original answer for older Ruby versions is below.


The problem with include (as shown in the following diagram) is that methods of a class cannot be overridden by modules included in that class (solutions follow the diagram): Ruby Method Lookup Flow

Solutions

  1. Subclass Date just for this one method:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> class MyDate < Date; include Foo; end
    #=> MyDate
    irb(main):003:0> MyDate.today.next(:world)
    #=> :world
    
  2. Extend just the instances you need with your own method:

    irb(main):001:0> require 'date'; module Foo; def next(a=:hi); a; end; end
    #=> nil
    irb(main):002:0> d = Date.today; d.extend(Foo); d.next(:world)
    #=> :world
    
  3. When including your module, perform a gross hack and reach inside the class and destroy the old 'next' so that yours gets called:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> module Foo
    irb(main):003:1>   def self.included(klass)
    irb(main):004:2>     klass.class_eval do
    irb(main):005:3*       remove_method :next
    irb(main):006:3>     end
    irb(main):007:2>   end
    irb(main):008:1>   def next(a=:hi); a; end
    irb(main):009:1> end
    #=> nil
    irb(main):010:0> class Date; include Foo; end
    #=> Date
    irb(main):011:0> Date.today.next(:world)
    #=> :world
    

    This method is far more invasive than just including a module, but the only way (of the techniques shown so far) to make it so that new Date instances returned by system methods will automatically use methods from your own module.

  4. But if you're going to do that, you might as well skip the module altogether and just go straight to monkeypatch land:

    irb(main):001:0> require 'date'
    #=> true
    irb(main):002:0> class Date
    irb(main):003:1>   alias_method :_real_next, :next
    irb(main):004:1>   def next(a=:hi); a; end
    irb(main):005:1> end
    #=> nil
    irb(main):006:0> Date.today.next(:world)
    #=> :world
    
  5. If you really need this functionality in your own environment, note that the Prepend library by banisterfiend can give you the ability to cause lookup to occur in a module before the class into which it is mixed.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • RUBY_DESCRIPTION => "ruby 1.8.7 (2011-02-18 patchlevel 334) [i386-mingw32]" I just realized something. My code is using the DateTime class, so I need to `require 'date'` (according to [this](http://whynotwiki.com/Ruby_/_Dates_and_times#Caveat:_you_may_need_to_require_certain_date_classes_for_certain_Date.2FTime_features)). Using exactly your example after this instruction, Ruby throws the error. I posted a [gist](https://gist.github.com/963819) because I couldn't post the whole code here. Thanks! – resilva87 May 10 '11 at 02:28
  • @kaos12 Thanks, I've edited my answer with various options available to you. – Phrogz May 10 '11 at 02:55
  • +1 How come I never came across this diagram before? That's amazing, nice work! – Jörg W Mittag May 10 '11 at 10:08
  • @Jörg Thanks :) There's also a [PDF version](http://phrogz.net/RubyLibs/RubyMethodLookupFlow.pdf) version available. – Phrogz May 10 '11 at 16:48
  • @Phrogz Thanks a lot! Your answer is very complete and easy to understand. I gotta say that I normally don't mess around with Ruby overriding feature, but after checking your diagram, it makes much more sense. Cheers! Just out of curiosity, is there any reason for this behavior change when using a subclass? Why the method lookup searches first the module? – resilva87 May 10 '11 at 18:21
  • 1
    @kaos12 There is no change in behavior. Methods from included modules always go 'above' the class they are included into. Try this: `class MyClass; end; [Object,Class,Module,MyClass].each{ |k| k.class_eval{ define_method(:foo){ puts "#{k}#foo"; super() unless k==Object }}}; MyClass.new.foo` Instance methods on a class with no included modules go straight to instance methods on `Object`. – Phrogz May 10 '11 at 21:24
  • @Phrogz oh, I see. I find amazing how much you can learn from Ruby community, many thanks mate! ;) – resilva87 May 11 '11 at 13:36
  • 1
    In the future, [Module#prepend](http://redmine.ruby-lang.org/issues/1102) might be worth looking into. – Andrew Grimm Jun 03 '11 at 02:55
  • 1
    [Module#prepend](http://www.ruby-doc.org/core-2.0/Module.html#method-i-prepend) is already available in Ruby 2.0 – Waiting for Dev... Jul 19 '13 at 14:03
7

The next method for Date is defined in the Date class and methods defined in a class take precedence over those defined in an included module. So, when you do this:

class Date
  include DateExtension
end

You're pulling in your version of next but the next defined in Date still takes precedence. You'll have to put your next right in Date:

class Date
  def next(symb=:day)
    dt = DateTime.now
      {:day   => Date.new(dt.year, dt.month, dt.day + 1),
       :week  => Date.new(dt.year, dt.month, dt.day + 7),
       :month => Date.new(dt.year, dt.month + 1, dt.day),
       :year  => Date.new(dt.year + 1, dt.month, dt.day)}[symb]
    end
end

From the the Programming Ruby chapter on Classes and Objects:

When a class includes a module, that module's instance methods become available as instance methods of the class. It's almost as if the module becomes a superclass of the class that uses it. Not surprisingly, that's about how it works. When you include a module, Ruby creates an anonymous proxy class that references that module, and inserts that proxy as the direct superclass of the class that did the including.

mu is too short
  • 426,620
  • 70
  • 833
  • 800