More Flexible Refinement Syntax
I am the maintainer of Ruby Facets, the core extensions library. For the next release of Facets I have long planned to provide support for Refinements. Unfortunately, after working with the code to determine what would be necessary to support them, I've come to the conclusion that it's just not reasonable to do so. The problem lies in the fact that Facets must still be backward compatible with it's "monkey-patch" usage. In fact, that usage is sometimes preferable b/c you can require once and don't have to write
using Foo in every file that a core extension might be needed. But, b/c of the syntax that refinements use, to support both patching and refining I would have to maintain TWO COPIES of every extension, which simply isn't practical.
For example, the normal definition of a String#foo:
class String def foo ... end end
And the refinement:
module Facets refine String do def foo ... end end end
There does not appear to be any reasonable way to have the definition defined once and still be able to be use it in either manner. (Also, I want to point out that refinements do not lend themselves to cherry picking specific methods per-file either.)
So, unless someone has a clever approach that I have not thought of, I wonder if it would not be a good idea to reconsider the syntax of refinements. Would it be possible to simplify the definition to use
class instead of
module Facets class String def foo ... end end end
And then allow
using Facets which would refine any common class is the scope. And further, allowing also
using Facets::String and even
using Facets::String::foo to cherry pick refinements? In addition, a way to "apply" a module as if it were evaluated in the scope. This would then allow the same code to be used either as a refinement or as an extension.
Alternatively, maybe refinements should just be a require --if they will forever remain at the file-level. Then no special syntax would be needed at all. Simply defining them in a separate file, e.g.
# string/foo.rb class String def foo ... end end
And then "using" them by file name instead would do the trick.
#3 [ruby-core:61840] Updated by Thomas Sawyer almost 3 years ago
If there "could be better design" could this ticket be set to feedback? Maybe others have some ideas about it.
Am I wrong to think that the most applicable use-cases for refinements by-far are the ActiveSupport and Facets core extension libraries? And if those two projects find it impractical to support refinements, then a better solution really does need to be found. Otherwise refinements will simply be an unused (hence useless) feature.
@Rodrigo Thanks for the suggestion. I have considered that. While most extensions would work fine, edge cases tend to make maintaining such a pre-processor a real headache. I won't rule it out, but that's not an approach I'd readily jump into.
#6 [ruby-core:71301] Updated by James Adam over 1 year ago
It might be possible to support both monkey-patching and refinements with a combination of modules, using prepend and refine:
class Target end # actual code module Behaviour def new_method 'facets-supplied behaviour' end end
In 'facets/refinements.rb' you could define the following:
# refinement support module Refinement refine Target do include Behaviour end end
And then use it as follows:
require 'facets/refinements' using Refinement Target.new.new_method # => 'facets-supplied behaviour'
To use the same code via monkey-patching, use
prepend; in 'facets/monkey-patch.rb':
# monkey-patch support Target.send(:prepend, Behaviour)
And then to use it:
require 'facets/monkey-patch' Target.new.new_method # => 'facets-supplied behaviour
prepend allows the module method definitions to take precedence over methods defined in the original class.
There's one significant problem doing this, which is that when you include a module via
refine, methods in that module cannot call other methods within that module. I believe this is a simple artefact of the lexical scope of those method definitions, but it probably does make it impractical to use this technique for very sophisticated changes.
#7 [ruby-core:71302] Updated by James Adam over 1 year ago
Another alternative is to slightly abuse the fact that refinements are also modules:
module Refinement Behaviour = refine Target do def new_method 'facets-supplied-behaviour' end end end
To use as a refinement:
using Refinement Target.new.new_method # => 'facets-supplied behaviour'
To use via monkey-patching:
Target.send(:prepend, Refinement::Behaviour) Target.new.new_method # => 'facets-supplied behaviour'
Using the module returned by
refine in this way seems a bit risky, but it appears to work.