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.
- Opt in. A file says using or it gets the standard library.
- Testable. The file that refines can be tested on its own, and the rest of the suite is unaffected.
- Composable. Two refinements on the same class are ordered by the using lines, like includes.
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:
- A test helper. A method that makes assertions read better, visible only in the test files.
- A compatibility shim. One file that adapts an old API, kept out of the rest of the process.
- A local experiment. A method tried in a script without touching the library for everyone.
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