Refinements

refine and using

A refinement is a monkey patch with a scope. refine defines methods on a class inside a module, and using switches the module on for the rest of the file:

module Shout refine String do def hi = "hi #{self}" end end using Shout p "x".hi

ruby refine.rb

"hi x"

The call is a normal method call; the refinement is the reason it resolves. Outside the file, String has no hi, which is the whole point.

The scope is the file

using is lexical: it applies from the line to the end of the file, and to blocks defined there. A class defined before the using does not see the refinement, even when it is called afterwards:

class Plain def call(string) = string.hi end using Shout p "x".hi begin Plain.new.call("y") rescue NoMethodError puts "class: no hi" end

ruby refine.rb

"hi x" class: no hi

The method body was compiled where hi did not exist, and nothing later adds it to that scope. That is the behaviour that makes a refinement safe: a library cannot change a method for code that never asked.

Why it does not leak

A monkey patch reopens the class for the whole process. A refinement is a separate lookup table attached to a scope, so two files can refine String differently and neither sees the other.

When to use it

Rails, RSpec and many gems still ship a monkey patch, because a refinement in a gem is useless to the code that requires it unless that code also writes using. The honest uses are:

What to watch for

using at the top level of a required file applies to that file, not to the file that required it. A helper meant for the caller has to be applied by the caller, which is why a gem rarely uses one for its public API.

A refinement cannot refine a method that the class does not have inside a method defined before the using. The rule is the same as the class above, and the error is a NoMethodError at the call, not at the definition.

Next: Garbage collection and memory

Article author: Arthur Isaev