I'm back to programming after some months of pause. The last thing I've heard about Ruby before pausing was Refinements. And I fell in love with it.
I found that idea so smart that I couldn't continue programming without it. I couldn't wait for Ruby 2.0. I had to implement it on my own.
Ruby 1.8.7 give us enough tools for designing a lexically scoped activation of refinements. I could use using with set_trace_func to detect the end of blocks (scopes), but I preferred to use enable and disable, because:
- it's simpler to implement;
- it's explicit and easy to read;
- the programmer has the freedom to enable and disable the refinements whenever he/she considers it necessary.
My solution is so simple that I called it "an ingenuous implementation". It has many differences from the original proposal, as I will discuss later, but it brings which I consider the most important feature to me: the refinements are limited to physical ranges within the text file. There's no outside consequences. Anyone can use my refined libraries with no (unpleasant) surprises. And the unrefined methods are not affected (if you're thinking about performance impact).
# Refinements for Ruby: an ingenuous implementation # # (c) 2012 Sony Fermino dos Santos # http://rubychallenger.blogspot.com/2012/12/refinements-in-ruby-ingenuous.html # # License: Public Domain # This software is released "AS IS", without any warranty. # The author is not responsible for the consequences of use of this software.
#
# This code is not intended to look professional,
# provided that it does what it is supposed to do. # # This software was little tested on Ruby 1.8.7 and 1.9.3, with success. # However, no heavy tests were made, e.g. threads, continuation, benchmarks, etc. # # The intended use is in the straightforward flux of execution. # # Instead of using +using+ as in the original proposal, here we use # Module#enable and Module#disable. They're lexically scoped by the # file:line of where they're called from. # # E.g.: Let StrUtils be a module which refine the String class. # module StrUtils # refine String do # def foo # #... # end # end # end # # Using it in the code snippets: # # StrUtils.enable # "abc".foo #=> works (foo is "visible") # def bar; puts "abc".foo; end #=> bar is defined where foo is "visible" # StrUtils.disable # "abc".foo #=> doesn't work (foo is "invisible") # bar #=> works, as bar was defined where foo is "visible" # def baz; puts "abc".foo; end # baz #=> doesn't work. # # You can enable and disable a module at any time, since you: # * enable and disable in this order, in the file AND in the execution flow; # * disable all modules that you enabled in the same file; # * don't reenable (or redisable) an already enabled (or disabled) module. # # See refine_test.rb for more examples. # Refinements is to avoid monkey patches, but # we need some minimal patching to implement it. class Module # Opens an enabled range for this module's refinements def enable info = ranges_info # there should be no open range raise "Module #{self} was already enabled in #{info[:file]}:#{info[:last]}" if info[:open] # range in progress info[:ranges] << info[:line] end # Close a previously opened enabled range def disable info = ranges_info # there must be an open range in progress raise "Module #{self} was not enabled in #{info[:file]} before line #{info[:line]}" unless info[:open] # beginning of range must be before end r_beg = info[:last] r_end = info[:line] raise "#{self}.disable in #{info[:file]}:#{r_end} must be after #{self}.enable (line #{r_beg})" unless r_end >= r_beg r = Range.new(r_beg, r_end) # replace the single initial line with the range, making sure it's unique info[:ranges].pop info[:ranges] << r unless info[:ranges].include? r end # Check whether a refined method is called from an enabled range def enabled? info = ranges_info info[:ranges].each do |r| case r when Range return true if r.include?(info[:line]) when Integer return true if info[:line] >= r end end false end private # Stores enabled line ranges of caller files for this module def enabled_ranges @enabled_ranges ||= {} end # Get the caller info in a structured way (hash) def caller_info # ignore internal calls (using skip would differ from 1.8.7 to 1.9.3) c = caller.find { |s| !s.start_with?(__FILE__, '(eval)') } and m = c.match(/^([^:]+):(\d+)(:in `(.*)')?$/) and {:file => m[1], :line => m[2].to_i, :method => m[4]} or {} end # Get line ranges info for the caller file def ranges_info ci = caller_info ranges = enabled_ranges[ci[:file]] ||= [] ci[:ranges] = ranges # check whether there is an opened range in progress for the caller file last = ranges[-1] if last.is_a? Integer ci[:last] = last ci[:open] = true end ci end # Here the original methods will be replaced with one which checks # whether the method is called from an enabled or disabled region, # and then decide which method to call. def refine klass, &blk modname = to_s mdl = Module.new &blk klass.class_eval do # Rename the klass's original (affected) methods mdl.instance_methods.each do |m| if method_defined? m alias_method "_#{m}_changed_by_#{modname}", m remove_method m end end # Include the refined methods include mdl end # Rename the refined methods and replace them with # a method which will check what method to call. mdl.instance_methods.each do |m| klass.class_eval <<-STR alias_method :_#{modname}_#{m}, :#{m} def #{m}(*args, &b) if #{modname}.enabled? _#{modname}_#{m}(*args, &b) else begin _#{m}_changed_by_#{modname}(*args, &b) rescue NoMethodError raise NoMethodError.new("Undefined method `#{m}' for #{klass}") end end end STR end end end
Here there are some examples:
#!/usr/bin/ruby require "./refine" class A def a 'a' end def b a + 'b' end end module A2 refine A do def a a + '2' # A#a, since here A2 is disabled end A2.enable # You must make A2 explicit here def d a + 'd' # A2#a, since here A2 is enabled end A2.disable end refine String do def length length + 1 # Original String#length, since A2 is disabled here end end end a = A.new str = 'abc' puts a.a # a puts a.b # ab puts str.length # 3 A2.enable class A def c a + 'c' # a2c, as A2 is enabled end end puts '' puts a.a # a2 puts a.b # ab (b was not refined nor defined where A2 is enabled) puts a.c # a2c puts a.d # a2d puts str.length A2.disable puts '' puts a.a # a puts a.b # ab puts a.c # a2c (it was defined where A2 is enabled) # puts a.d # NoMethodError, since A2 is disabled puts str.length # In-method enabling test def x(y) A2.enable puts y.a # a2 A2.disable end x(a) # a2 x(a) # enabling multiple times at same line with no error # Lazy enabling test def e A2.enable end def z(y) puts y.a # defined between enable and disable, but affected only after running e() and d() end def d A2.disable end z(a) # a e # now, activating the range for refinements d z(a) # a2 def e2 A2.enable end e2 # running before d(), but... d # error, as you are enabing *after* the disable (physically in the text file)
Differences from the original proposal:
- enable and disable instead of using;
- calls to refined methods only works if it's within the enabled range in the file; so subclasses won't be affected unless their code is in an enabled range;
- super doesn't work for calling the original methods, but you can call it by its name from an un-enabled range; or by calling the renamed methods (see the code for refine).
I think this solution is good enough for me, and I guess it won't have the evil side of refinements which was very well discussed in this post (and I agree).
I'm open to discuss about errors, consequences and improvements to my code; feel free. ;-)
No comments:
Post a Comment