Do refinements apply only to instance methods?
Asked Answered
M

2

16

I'm trying to understand Ruby's refinements feature, and I've encountered a scenario I don't understand.

Take this example code:

class Traveller
  def what_are_you
    puts "I'm a Backpacker"
  end

  def self.preferred_accommodation
    puts "Hostels"
  end
end


module Refinements
  module Money
    def what_are_you
      puts "I'm a cashed-up hedonist!"
    end

    module ClassMethods
      def preferred_accommodation
        puts "Expensive Hotels"
      end
    end

    def self.included(base)
      base.extend ClassMethods
    end
  end

  refine Traveller do
    include Money
  end
end

Now, when I do this in the REPL:

Traveller.new.what_are_you         # => I'm a Backpacker
Traveller.preferred_accommodation  # => Hostels

using Refinements

Traveller.new.what_are_you         # => I'm a cashed-up hedonist!
Traveller.preferred_accommodation  # => Hostels (???)

Why is #what_are_you refined, but .preferred_accommodation is not?

Mizuki answered 8/1, 2015 at 5:22 Comment(3)
Traveller.preferred_accommodation is a class method. Traveller.new.preferred_accommodation will print what you expect (since you have base class extended on inclusion.) But Traveller is an instance of Class class. Whether you want Traveller.preferred_accommodation refined, you are to refine Class class.Officeholder
@mudasobwa, you could do that, but wouldn't that make the refined versions of methods available to all classes? Isn't that the same problem as creating class methods by defining instance methods on the class Class: they become class methods for all classes? I can't see any advantage of doing that over refining singleton classes, and obvious disadvantages.Toffee
@CarySwoveland Surely you are right. I would say it mustn’t be done with Class refinement; the reason I dropped the comment and not an answer is: I wanted to shed a light on what’s going on but not to give a how-to recipe.Officeholder
T
20

As @MasashiMiyazaki explained, you have to refine two classes: Traveller and Traveller's singleton class. That actually allows you to simplify your code quite a bit:

module Money
  refine Traveller do
    def what_are_you
      puts "I'm a cashed-up hedonist!"
    end
  end

  refine Traveller.singleton_class do
    def preferred_accommodation
      puts "Expensive Hotels"
    end
  end
end

Traveller.new.what_are_you         #=> I'm a Backpacker
Traveller.preferred_accommodation  #=> Hostels

using Money
Traveller.new.what_are_you         #=> I'm a cashed-up hedonist!
Traveller.preferred_accommodation  #=> Expensive Hotels

Moreover, by putting the above three statements in a module, the refined versions of the two methods are confined to that module:

module M
  using Money
  Traveller.new.what_are_you         #=> I'm a cashed-up hedonist!
  Traveller.preferred_accommodation  #=> Expensive Hotels
end

Traveller.new.what_are_you           #=> I'm a Backpacker
Traveller.preferred_accommodation    #=> Hostels
Toffee answered 8/1, 2015 at 7:33 Comment(1)
I understood that "class methods" are just "instance methods on the singleton class", but somehow missed that implication. Thinking about Ruby's object model some more, this makes sense. Thanks!Mizuki
T
6

You need to call refine Traveller with singleton_class scope to overwrite class methods. By adding the following code to your Refinements module instead of self.included, you can get the expected result.

module Refinements
  refine Traveller.singleton_class do
    include Money::ClassMethods
  end
end

This article(http://timelessrepo.com/refinements-in-ruby) will help you to understand Refinements more.

Towards answered 8/1, 2015 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.