M
Mark Hubbart
Hi all,
A common ruby idiom for redefining a method in a class is to alias it
to give it a new name before defining the replacement:
# allow integer indices to wrap.
# "test"[6] #=>"e"
class String
alias ld_brackets :[]
def [](idx, num=nil)
idx = ((idx % size) + idx) % size if idx.kind_of? Fixnum
num ? old_brackets(idx, num) : old_brackets(idx)
end
end
However, this breaks if it is done a second time, with the same alias:
# allow ranges to wrap around
# "abcd"[-2..1] #=>"cdab"
class String
alias ld_brackets :[]
def [](idx, num=nil)
if Range === idx and idx.first < 0 and idx.end > 0
end_range = idx.exclude_end?? (0...idx.end) : (0..idx.end)
self[idx.first..-1] + self[end_range]
else
num ? old_brackets(idx, num) : old_brackets(idx)
end
end
end
then in irb:
irb(main):004:0>>> "abcd"[3]
(eval):4:in `old_brackets': stack level too deep (SystemStackError)
from (eval):5:in `old_brackets'
from (eval):5:in `old_brackets'
<snip long stack error>
This can be a problem. If those two definitions occurred in two
separate libraries, then the maintainers of both libraries would have
to get together (once the problem is identified) and modify their code
just so the libraries can be used together. It could discourage people
from using what is (IMHO) one of Ruby's greatest strengths... the
ability to easily redefine methods.
One solution comes from the common OO method/construct: super().
Calling super() lets you call the parent class' version of a particular
function; it's smart, and knows to call the same function that you are
currently in. Since most OO languages (that I've worked with) have
non-redefinable classes, super() is enough. But when you redefine a
method in Ruby, there's no way of getting to the previously defined
method. I propose a new method/construct, tentatively named call_up(),
which would give access to the older versions of a method; and a
companion method redefine() which stores the method in a stack before
undefing it.
Here's a partial example of a hacked solution. Additional coding would
be needed to support the same thing in modules and singleton classes, I
but it shows a rough idea of the way it would work.
class Object
def redefine(sym)
if implements? sym
@@__redefined_methods[sym] ||= []
@@__redefined_methods[sym].push method(sym)
undef sym
else raise ArgumentError, "not a valid method"
end
end
def call_up(*args)
# Deep Magic Goes Here:
# - detects the calling method
# - detects if it is in the __redefined_methods stack
# - determines which method version was defined immediately
previous
# - calls that method with *args
end
end
Each class (and singleton object) would keep track of the explicitly
redefined methods, and allow a super()ish way of calling the next
method up. This could allow the above method redefinition code to look
like this:
# allow integer indices to wrap.
# "test"[6] #=>"e"
class String
redefine :[]
def [](idx, num=nil)
idx = ((idx % size) + idx) % size if idx.kind_of? Fixnum
num ? call_up(idx, num) : call_up(idx)
end
end
# allow ranges to wrap around
# "abcd"[-2..1] #=>"cdab"
class String
redefine :[]
def [](idx, num=nil)
if Range === idx and idx.first < 0 and idx.end >= 0
end_range = idx.exclude_end?? (0...idx.end) : (0..idx.end)
self[idx.first..-1] + self[end_range]
else
num ? call_up(idx, num) : call_up(idx)
end
end
end
If we could do this, it would eliminate any worries about conflicting
method names, and would free Ruby programmers to redefine methods
whenever it makes sense. I don't think it would introduce any real
added complexity, and it would standardize how to redefine a method and
call the original.
For an even more real-world example, consider all the different numeric
types defined in the stdlib math libraries. If you want to create a new
Numeric, and want Math.sqrt() to support it, you have to redefine
Math.sqrt. But if it just happens that someone else did the same thing,
in the same way, then everything will go horribly wrong the moment you
use Math.sqrt() after require()ing both files.
Comments? Questions? Flames?
--Mark
A common ruby idiom for redefining a method in a class is to alias it
to give it a new name before defining the replacement:
# allow integer indices to wrap.
# "test"[6] #=>"e"
class String
alias ld_brackets :[]
def [](idx, num=nil)
idx = ((idx % size) + idx) % size if idx.kind_of? Fixnum
num ? old_brackets(idx, num) : old_brackets(idx)
end
end
However, this breaks if it is done a second time, with the same alias:
# allow ranges to wrap around
# "abcd"[-2..1] #=>"cdab"
class String
alias ld_brackets :[]
def [](idx, num=nil)
if Range === idx and idx.first < 0 and idx.end > 0
end_range = idx.exclude_end?? (0...idx.end) : (0..idx.end)
self[idx.first..-1] + self[end_range]
else
num ? old_brackets(idx, num) : old_brackets(idx)
end
end
end
then in irb:
irb(main):004:0>>> "abcd"[3]
(eval):4:in `old_brackets': stack level too deep (SystemStackError)
from (eval):5:in `old_brackets'
from (eval):5:in `old_brackets'
<snip long stack error>
This can be a problem. If those two definitions occurred in two
separate libraries, then the maintainers of both libraries would have
to get together (once the problem is identified) and modify their code
just so the libraries can be used together. It could discourage people
from using what is (IMHO) one of Ruby's greatest strengths... the
ability to easily redefine methods.
One solution comes from the common OO method/construct: super().
Calling super() lets you call the parent class' version of a particular
function; it's smart, and knows to call the same function that you are
currently in. Since most OO languages (that I've worked with) have
non-redefinable classes, super() is enough. But when you redefine a
method in Ruby, there's no way of getting to the previously defined
method. I propose a new method/construct, tentatively named call_up(),
which would give access to the older versions of a method; and a
companion method redefine() which stores the method in a stack before
undefing it.
Here's a partial example of a hacked solution. Additional coding would
be needed to support the same thing in modules and singleton classes, I
but it shows a rough idea of the way it would work.
class Object
def redefine(sym)
if implements? sym
@@__redefined_methods[sym] ||= []
@@__redefined_methods[sym].push method(sym)
undef sym
else raise ArgumentError, "not a valid method"
end
end
def call_up(*args)
# Deep Magic Goes Here:
# - detects the calling method
# - detects if it is in the __redefined_methods stack
# - determines which method version was defined immediately
previous
# - calls that method with *args
end
end
Each class (and singleton object) would keep track of the explicitly
redefined methods, and allow a super()ish way of calling the next
method up. This could allow the above method redefinition code to look
like this:
# allow integer indices to wrap.
# "test"[6] #=>"e"
class String
redefine :[]
def [](idx, num=nil)
idx = ((idx % size) + idx) % size if idx.kind_of? Fixnum
num ? call_up(idx, num) : call_up(idx)
end
end
# allow ranges to wrap around
# "abcd"[-2..1] #=>"cdab"
class String
redefine :[]
def [](idx, num=nil)
if Range === idx and idx.first < 0 and idx.end >= 0
end_range = idx.exclude_end?? (0...idx.end) : (0..idx.end)
self[idx.first..-1] + self[end_range]
else
num ? call_up(idx, num) : call_up(idx)
end
end
end
If we could do this, it would eliminate any worries about conflicting
method names, and would free Ruby programmers to redefine methods
whenever it makes sense. I don't think it would introduce any real
added complexity, and it would standardize how to redefine a method and
call the original.
For an even more real-world example, consider all the different numeric
types defined in the stdlib math libraries. If you want to create a new
Numeric, and want Math.sqrt() to support it, you have to redefine
Math.sqrt. But if it just happens that someone else did the same thing,
in the same way, then everything will go horribly wrong the moment you
use Math.sqrt() after require()ing both files.
Comments? Questions? Flames?
--Mark