Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use refinements dynamically

Tags:

ruby

Trying to make sense of this 'refinements' business.

I'm making a module which refines a core class:

module StringPatch
  refine String do
    def foo
      true
    end
  end
end

Then a class to use the refinement

class PatchedClass
end

PatchedClass.send :using, StringPatch

I get this error:

RuntimeError: Module#using is not permitted in methods

How can I make this work? I am trying to dynamically patch core classes in a certain scope only. I want to make the patches available in the class and instance scope.

like image 506
max pleaner Avatar asked Dec 07 '16 21:12

max pleaner


3 Answers

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.

like image 58
Eric Duminil Avatar answered Oct 07 '22 09:10

Eric Duminil


Refinements are strictly scoped and can't be used dynamically as it stands. I get around this by structuring modules such that I can use them a refinement or as a monkey patch when needed. There are still limitations to this approach, and with refinements in general, but still handy.

module StringPatch      
    def foo
      true
    end

    refine String do
      include StringPatch
    end
 end

Used as a refinement

class PatchedClass
   using StringPatch
end

Used as a single instance patch

obj = PatchedClass.new
obj.extend(StringPatch)

You can of course also extend the whole class as a monkey patch but then you pollute the class forever and for all eternity.

PatchedClass.prepend(StringPatch)
like image 36
kawikak Avatar answered Oct 07 '22 08:10

kawikak


I appreciate the other answers but I think I have to add my own.

It seems there's very little leeway with using - the error that it can't be used in methods is serious. The best wiggle room I found is to iteratively call it, passing a variable as argument.

So if I have two patch classes, I can make a constant Patches, a list that includes both. Then in the class/module I want to load the patches, I can run the iterative using.

module StringPatch
  refine String do
    def patch; :string_patch; end
  end
end

module HashPatch
  refine Hash do
    def patch; :hash_patch; end
  end
end

Patches = [StringPatch, HashPatch]
class C
  Patches.each { |x| using x }
  def self.test
    [''.patch, {}.patch]
    new.test
  end
  def test
    [''.patch, {}.patch]
  end
end

C.test

The using does make the patches available in both instance and class scope.

The limitation is that there is no way to abstract away the using call.

I can't say Patches.each &method(:using)

or move Patches.each { |x| using x } to a method,

or use eval "Patches.each { |x| using x}"

like image 2
max pleaner Avatar answered Oct 07 '22 07:10

max pleaner