Skip to content

Instantly share code, notes, and snippets.

@kputnam
Last active April 13, 2022 23:49
Show Gist options
  • Save kputnam/c77e69264a2dc878ed8e to your computer and use it in GitHub Desktop.
Save kputnam/c77e69264a2dc878ed8e to your computer and use it in GitHub Desktop.

Notes on Ruby's method refinement

The example below creates a refinement module which adds a method to instances of Integer. In the Test class, we bring it into scope by declaring using Refinements. Next, we create an instance method using def_delegators named Test#xxx that calls @k.xxx, which should resolve to the refinement method we added to Integer. Finally, we "manually" create essentially the same method, this time named yyy, but we define it "manually" without using def_delegators.

require "forwardable"

module Refinements
  refine Integer do
    def xxx
      :xxx
    end
  end
end

class Test
  using  Refinements
  extend Forwardable
  def_delegators :@k, :xxx
  
  def initialize
    @k = 100
  end
  
  def yyy
    @k.xxx
  end
end

The test snippet below demonstrates ordinary methods (like yyy) inherit the refinement methods from the scope in which they are defined. However, methods defined by class_eval or module_eval do not inherit the refinements which are in the lexical scope of the class_eval caller.

Instead, methods defined by class_eval or module_eval inherit the refinements from the call site of class_eval, which would be Forwardable, not Test. Since module Forwardable; ...; end does not have a using Refinements declaration, our xxx method doesn't see them.

p Test.new.yyy #=> :xxx
p Test.new.xxx #=> undefined method `xxx` for 100:Fixnum (NoMethodError)

respond_to? on Refinement Methods

The code above below creates a refinement method named Integer#xxx that is imported by the Test class. We define a method named Test#xxx that delegates to Integer#xxx, which is resolved to our refinement method. The second method zzz simply returns whether an Integer responds to a method named xxx.

require "forwardable"

module Refinements
  refine Integer do
    def xxx
      :xxx
    end
  end
end

class Test
  using Refinements

  def xxx
    100.xxx
  end

  def zzz
    100.respond_to?(:xxx)
  end
end

The first method call works as expected, it calls Integer#xxx which was brought into scope by Test. However, the second method call is surprising, because within the body of zzz, the refinement is in scope and the instance does respond to that method (we just demonstrated that).

p Test.new.xxx #=> :xxx
p Test.new.zzz #=> false!?

This is explained in the documentation like so:

Indirect Method Calls

When using indirect method access such as Kernel#send, Kernel#method or Kernel#respond_to? refinements are not honored for the caller context during method lookup.

This behavior may be changed in the future.

Note using send or __send__ to invoke refinement methods fails with NoMethodError for the same reason. There generally is no workaround, though sometimes using eval, module_eval, and class_eval, you can invoke methods without using __send__.

The best solution seems to be to not use respond_to? for refinement methods, make the method call unconditionally. If you catch a NoMethodError, then it could be because the refinement method isn't defined (though it could also be for a different reason!).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment