Ruby refinements gotchas
Asked Answered
S

1

9

In Metaprogramming Ruby 2 in Chapter 2 at the "Refinements" section I found the following piece of Ruby code:

class MyClass 
  def my_method
    "original my_method()"
  end

  def another_method 
    my_method
  end

end

module MyClassRefinement 
  refine MyClass do
    def my_method
      "refined my_method()"
    end 
  end
end

using MyClassRefinement
MyClass.new.my_method # => "refined my_method()"
MyClass.new.another_method # => "original my_method()" - How is this possible?

According to the author:

However, the call to another_method could catch you off guard: even if you call another_method after using, the call to my_method itself happens before using — so it calls the original, unrefined version of the method.

This totally trips me up.

Why MyClass.new.another_method prints "original my_method()" since its used after using MyClassRefinement and what is the author trying to say here?

Could anyone provide a more intuitive/better explanation?

Thanks.

Siren answered 7/10, 2017 at 18:30 Comment(1)
It's probably because the remapping introduced by refinements only applies to a particular scope, and within that original definition the scope is unaffected.Fuddle
U
5

The best explanation I can find is from the docs:

Refinements are lexical in scope. Refinements are only active within a scope after the call to using. Any code before the using statement will not have the refinement activated.

That means your refinement method must be invoked somewhere after the call to using. The actual location of the method's invocation is what matters, not how the method was invoked or from where the method was invoked.


Here's what happens.

  1. using i.e. using MyClassRefinement activates the my_method refinement.
  2. MyClass.new.my_method is executed.
  3. A method lookup ensues from the exact point of invocation:

When looking up a method for an instance of class C Ruby checks:

  • If refinements are active for C, in the reverse order they were activated
    • The prepended modules from the refinement for C
    • The refinement for C
    • The included modules from the refinement for C
  • The prepended modules of C
  • C
  • The included modules of C
  1. Refinements are active, and my_method returns the code from the refinement "refined my_method()"
  2. MyClass.new.another_method is executed.
  3. The method lookup ensues from the exact point of invocation.
  4. Refinements are active at this point of invocation, but the another_method is not a refinement, so Ruby looks for another_method in the class MyClass and finds it.
  5. Inside of the class method another_method, the method my_method is found and invoked.
  6. The method lookup ensues from the exact point of invocation.
  7. At the point of invocation there are no refinements active, because there have been no calls to using above the line (i.e. physically prior to) where my_method is invoked. Ruby goes on to look for my_method in the class MyClass and finds it.
  8. my_method returns the code from the class method "original my_method()".

We can make a simple comparison. Let's say I have one isolated file.rb with the following code:

puts puppy
puppy = 'waggle'

puppy can't be used before it is defined. The variable is lexically scoped, and its use depends on the location of its definition in the isolated file.rb.

Similarly, a refinement cannot be invoked until it has been activated via using on a previous line (or somewhere physically previous within the source code file). The refinement is lexically scoped.

From Wikipedia

In languages with lexical scope (also called static scope), name resolution depends on the location in the source code and the lexical context, which is defined by where the named variable or function is defined...

Lexical resolution can be determined at compile time, and is also known as early binding, while dynamic resolution can in general only be determined at run time, and thus is known as late binding.


Your specific issue with refinements is discussed in the last section of this article. The author explains as well how the using statement's physical location in the file determines whether or not a refinement is active.

Utricle answered 7/10, 2017 at 20:52 Comment(4)
Thank you! It makes total sense now! Lexical scoping was the true culprit.Siren
...and your references helped a lot!Siren
No problem :) I'm glad it helpedUtricle
@kstratis: Just remember that the whole point of refinements is that they provide monkey patching without affecting other code. That's precisely what you are seeing here. You are monkey patching my_method without affecting another_method.Linus

© 2022 - 2024 — McMap. All rights reserved.