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