Better Way to Turn a Ruby Class into a Module Than Using Refinements

Using refinements to patch a core Module such as Kernel

Modules can be refined as of ruby 2.4:

Module#refine accepts a module as the argument now. [Feature #12534]

The old caveat ("Refinements only modify classes, not modules so the argument must be a class") no longer applies (although it wasn't removed from the documentation until ruby 2.6).

Example:

module ModuleRefinement
refine Enumerable do
def tally(&block)
block ||= ->(value) { value }
counter = Hash.new(0)
each { |value| counter[block[value]] += 1 }
counter
end
end
end

using ModuleRefinement

p 'banana'.chars.tally # => {"b"=>1, "a"=>3, "n"=>2}

Ruby refinements subtleties

Context (or binding) is the reason why module_eval works and yield doesn't in your last set of examples. It actually has nothing to do with refinements, as demonstrated below.

Starting with module_eval:

class Foo
def run(&block)
self.class.module_eval(&block)
end
end

foo = Foo.new
foo.run {
def hello
"hello"
end
}

puts foo.hello # => "hello"
puts hello => # '<main>': undefined method 'hello' for main:Object (NameError)

In Foo#run we call module_eval on Foo. This switches the context (self) to be Foo. The result is much like we had simple defined hello inside of class Foo originally.

Now let's take a look at yield:

class Foo
def run
yield
end
end

foo = Foo.new
foo.run {
def hello
"hello"
end
}

puts hello # => "hello"
puts foo.hello # => '<main>': private method 'hello' called for ...

yield simply invokes the block in its original context, which in this example would be <main>. When the block is invoked, the end result is exactly the same as if the method were defined at the top level normally:

class Foo
def run
yield
end
end

foo = Foo.new

def hello
"hello"
end

puts hello # => "hello"
puts foo.hello # => '<main>': private method 'hello' called for ...

You might notice that foo seems to have the hello method in the yield examples. This is a side effect of defining hello as a method at the top level. It turns out that <main> is just an instance of Object, and defining top level methods is really just defining private methods on Object which nearly everything else ends up inheriting. You can see this by opening up irb and running the following:

self       # => main
self.class # => Object

def some_method
end

"string".method(:some_method) # => #<Method: String(Object)#some_method>

Now back to your examples.

Here's what happens in the yield example:

def ref_module1(klass)
Module.new do
refine(klass) {
yield
}
end
end

class Receiver1
# like my yield example, this block is going to
# end up being invoked in its original context
using ref_module1(Base) {
def foo
"I'm defined on Receiver1"
end
}

def bar
# calling foo here will simply call the original
# Base#foo method
Base.new.foo
end
end

# as expected, if we call Receiver1#bar
# we get the original Base#foo method
Receiver1.new.bar # => "foo"

# since the block is executed in its original context
# the method gets defined in Receiver1 -- its original context
Receiver1.new.foo # => "I'm defined on Receiver1"

As for module_eval, it works in your examples because it causes the block to be run in the context of the new module, rather than on the Receiver1 class.

How to refine module method in Ruby?

This piece of code will work:

module Math
def self.pi
puts 'original method'
end
end

module RefinementsInside
refine Math.singleton_class do
def pi
puts 'refined method'
end
end
end

module Main
using RefinementsInside
Math.pi #=> refined method
end

Math.pi #=> original method

Explanation:

Defining a module #method is equivalent to defining an instance method on its #singleton_class.

How to use refinements dynamically

As far as I know, the refinement is active until the end of the script when using is in main, and until the end of the current Class/Module definition when using is in a Class or Module.

module StringPatch
refine String do
def foo
true
end
end
end

class PatchedClass
using StringPatch
puts "test".foo
end

class PatchedClass
puts "test".foo #=> undefined method `foo' for "test":String (NoMethodError)
end

This would mean that if you manage to dynamically call using on a Class or Module, its effect will be directly removed.

You cannot use refine in methods, but you can define methods in a Class that has been refined :

class PatchedClass
using StringPatch
def foo
"test".foo #=> true
end
end

class PatchedClass
def bar
"test".foo
end
end

patched = PatchedClass.new
puts patched.foo #=> true
puts patched.bar #=> undefined method `foo' for "test":String (NoMethodError)

For your questions, this discussion could be interesting. It looks like refinements are restricted on purpose, but I don't know why :

Because refinement activation should be as static as possible.

Add method to a class which can only be accessed inside specific class

You could use refinements for this:

Due to Ruby's open classes you can redefine or add functionality to existing classes. This is called a “monkey patch”. Unfortunately the scope of such changes is global. All users of the monkey-patched class see the same changes. This can cause unintended side-effects or breakage of programs.

Refinements are designed to reduce the impact of monkey patching on other users of the monkey-patched class. Refinements provide a way to extend a class locally. Refinements can modify both classes and modules.

Something like this:

module HashPatches
refine Hash do
def new_hash_method
# ...
end
end
end

and then:

class YourClass
using HashPatches

def m
{}.new_hash_method
end
end

That would let you call YourClass.new.m (which would use new_hash_method) but it wouldn't pollute Hash globally so outside YourClass, some_hash.new_hash_method would be a NoMethodError.

Reading:

  • Official Refinements docs
  • Refinements spec

Using Refinements Hierarchically

Wow, this was really interesting to play around with! Thanks for asking this question! I found a way that works!

module M
refine String do
alias :dbl_eql :==
def ==(other)
downcase.dbl_eql(other.downcase)
end
end

refine Array do
def ==(other)
zip(other).all? {|x, y| x == y}
end
end
end

a = [[1, "a"],
[2, "b"],
[3, "c"],
[4, "d"]]

b = [[1, "AA"],
[2, "B"],
[3, "C"],
[5, "D"]]

using M

a.zip(b).count { |ae,be| ae == be } # 2

Without redefining == in Array, the refinement won't apply. Interestingly, it also doesn't work if you do it in two separate modules; this doesn't work, for instance:

module M
refine String do
alias :dbl_eql :==
def ==(other)
downcase.dbl_eql(other.downcase)
end
end
end

using M

module N
refine Array do
def ==(other)
zip(other).all? {|x, y| x == y}
end
end
end

a = [[1, "a"],
[2, "b"],
[3, "c"],
[4, "d"]]

b = [[1, "AA"],
[2, "B"],
[3, "C"],
[5, "D"]]

using N

a.zip(b).count { |ae,be| ae == be } # 0

I'm not familiar enough with the implementation details of refine to be totally confident about why this behavior occurs. My guess is that the inside of a refine block is treated sort of as entering a different top-level scope, similarly to how refines defined outside of the current file only apply if the file they are defined in is parsed with require in the current file. This would also explain why nested refines don't work; the interior refine goes out of scope the moment it exits. This would also explain why monkey-patching Array as follows works:

class Array
using M

def ==(other)
zip(other).all? {|x, y| x == y}
end
end

This doesn't fall prey to the scoping issues that refine creates, so the refine on String stays in scope.

How can super within a refinement call an overridden method?

Based on the documentation, the method lookup for a refinement's built in behaviour is the same as you have observed.

Your assumption was correct that it is not typical inheritance, that can be seen by invoking superclass

class C
def foo
puts "C#foo"
end
end

module M
refine C do
def foo
puts "C#foo in M"
puts "class: #{self.class}"
puts "superclass: #{self.class.superclass}"
super
end
end
end

using M

x = C.new

x.foo

The output:

C#foo in M
class: C
superclass: Object
C#foo

best way to organize a long piece of code in ruby refinement block

Here's a general pattern I ended up using. Basically I found no workaround for using global identifiers at some level. But this can be done fairly cleanly by making those globals classes/modules. This will be more clear as an example:

module StringPatches

def self.non_empty?(string)
!string.empty?
end

def non_empty?
StringPatches.non_empty?(self)
end

def non_non_empty?
!StringPatches.non_empty?(self)
end

refine String do
include StringPatches
end

end

class Foo
using StringPatches
puts "asd".non_empty? # => true
puts "asd".non_non_empty? # => false
end

The class methods on StringPatches don't get exported to using. But since classes/modules are constants (globals) they can be accessed from anywhere.



Related Topics



Leave a reply



Submit