max pleaner
max pleaner

Reputation: 26758

Using refinements to patch a core Module such as Kernel

I'm going through the Facets API and picking some methods to include in my refinement-compatible patch library.

I've hit a snag trying to patch Kernel. It's a module, whereas the other stuff I've patched has been classes (String, Array, etc.)


Here's proof that can't can't be refined using my standard approach for core classes:

module Patch
  refine Kernel do
    def patched?
      true
    end
  end
end

# TypeError: wrong argument type Module (expected Class)
# from (pry):16:in `refine' 

I've also tried wrapping the Kernel module in a class, and changing the global reference to Kernel to that class.

class MyKernel
  include Kernel
  extend Kernel
end

# not sure if Object::Kernel is really the global reference
Object::Kernel = MyKernel

module Patch
  refine MyKernel do
    def patched?
      true
     end
  end
end

class Test
  using Patch
  patched?
end
# NoMethodError: undefined method `patched?' for Test:Class
# from (pry):15:in `<class:Test>'

In this case I could successfully get the same functionality by replacing Kernel with Object:

module Patch
  refine Object do
    def patched?
      true
     end
  end
end

class Test
  using Patch
  patched?
end

But I'm not sure if I could get this equivalency with other core modules such as Enumerable.

Upvotes: 3

Views: 441

Answers (2)

chocolateboy
chocolateboy

Reputation: 1816

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}

Upvotes: 5

max pleaner
max pleaner

Reputation: 26758

As I mentioned in the question, one can perform the functional equivalent of extending the Kernel module by instead using the Object class.

The other example I gave was the Enumerable module, which it turns out can be practically extended through the Enumerator class:

module Patch
  refine Enumerator do
    def patched?
      true
    end
  end
end

class Test
  using Patch
  Array.new.to_enum.patched?
end

So I guess a workable solution could be to not try and turn core modules into classes, but instead extend classes that already include them.

In this case I could check with Enumerator < Enumerable which returns true because the Enumerator class includes the Enumerable module (though it doesn't check if it's been extended)


To update after looking at the corefines source, I found a helpful method for finding all the classes that include a Module

classes_including_module

Upvotes: 1

Related Questions