VOOZH about

URL: https://rubychallenger.blogspot.com/search/label/scope

⇱ The Ruby Challenger: scope


Showing posts with label scope. Show all posts
Showing posts with label scope. Show all posts

Thursday, December 13, 2012

Refinements in Ruby: an ingenuous implementation

UPDATE: I've worked on Namebox, an improved way to protect methods from changes, inspired on this implementation of Refinements.

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. ;-)

Sunday, April 3, 2011

Swapping variables

Today I was learning about binding, and the example showed in that page is how to use binding to swap variables.

The text ended up with
swap(ref{:a}, ref{:b})

However, I show here a less verbose (and simpler) swap (without binding, references, etc.):

swap = lambda do |x, y|
 eval "#{x}, #{y} = #{y}, #{x}"
end

a = 'hihaha'
b = 33

swap[:a, :b]

puts a #=> 33
puts b #=> 'hihaha'

I guess the former example is still valid as didatic text about binding.
Subscribe to: Posts (Atom)