Using an UnboundMethod instead of an alias to redefine a method

Discussion in 'Ruby' started by Daniel Berger, Dec 27, 2006.

  1. Hi all,

    I came across a technique for aliasing methods that I have never seen
    before [1] and was just too good to pass up. But first, the setup.

    If you want to redefine a method of an existing class, the traditional
    approach is to alias the method first, then call the alias as needed.
    For example, if you want to redefine Hash#[]= so that it stores
    multiple values as a list instead of overwriting the existing value you
    might do this:

    class Hash
    alias :eek:ld_hset :[]=

    def []=(key, value)
    if self[key]
    self[key] << value
    else
    self.old_hset(key, [value])
    end
    end
    end

    hash = {}
    hash['a'] = 1
    hash['a'] = 2
    hash['a'] # [1,2]

    That works well enough. But, it's not ideal. Why? First, because now
    you've got an aliased method laying around that we really don't want
    exposed to the public. Second, this could cause a problem if anyone
    were to ever define an old_hset method or alias themselves. Unlikely,
    but possible.

    The solution that Martin came up with is to use an UnboundMethod like
    so:

    class Hash
    hset = self.instance_method:)[]=)

    define_method:)[]=) do |key, value|
    if self[key]
    self[key] << value
    else
    hset.bind(self).call(key, [value])
    end
    end
    end

    So, now our custom Hash#[]= method is bound to an UnboundMethod that no
    one else has access to (see the first link below for a better
    explanation). Pretty neat, eh? Is there any downside to this approach?
    If not, it seems this technique ought to be filed under 'best
    practices', and perhaps even abstracted somehow in the core itself.

    Many thanks to Jay Fields [2], whose blog entry led me back to the
    original entry by Martin Traverso.

    Regards,

    Dan

    [1] http://split-s.blogspot.com/2006/01/replacing-methods.html
    [2]
    http://jayfields.blogspot.com/2006/12/ruby-alias-method-alternative.html
     
    Daniel Berger, Dec 27, 2006
    #1
    1. Advertisements

  2. Daniel Berger

    ara.t.howard Guest

    here, however, you've lost any block that goes with the method ;-(
    there are several problems related to scoping:

    http://groups-beta.google.com/group...st&q=harp+push_method&rnum=1#09a22a5ca639834f

    regards.

    -a
     
    ara.t.howard, Dec 27, 2006
    #2
    1. Advertisements

  3. Daniel Berger

    Pit Capitain Guest

    Dan, this is a nice technique indeed (not new, but still nice), but it
    comes with a performance penalty:

    class HashUsingAlias < Hash
    alias :eek:ld_hset :[]=

    def []=(key, value)
    self.old_hset(key, value)
    end
    end

    class HashUsingBind < Hash
    hset = self.instance_method:)[]=)

    define_method:)[]=) do |key, value|
    hset.bind(self).call(key, value)
    end
    end

    require "benchmark"

    def bm_report bm, title, hash_class
    hash = hash_class.new
    bm.report title do
    100_000.times do
    hash[ 1 ] = 1
    end
    end
    end

    Benchmark.bmbm do |bm|
    bm_report bm, "original", Hash
    bm_report bm, "alias", HashUsingAlias
    bm_report bm, "bind", HashUsingBind
    end

    On my system, I get the following results:

    user system total real
    original 0.062000 0.000000 0.062000 ( 0.062000)
    alias 0.141000 0.000000 0.141000 ( 0.140000)
    bind 0.656000 0.000000 0.656000 ( 0.657000)

    Regards,
    Pit
     
    Pit Capitain, Dec 27, 2006
    #3
  4. Daniel Berger

    ara.t.howard Guest

    mine is slowest of all, however it preserves blocks: see if you can speed it
    up:


    harp:~ > cat a.rb
    class HashUsingAlias < Hash
    alias :eek:ld_hset :[]=

    def []=(key, value)
    self.old_hset(key, value)
    end
    end

    class HashUsingBind < Hash
    hset = self.instance_method:)[]=)

    define_method:)[]=) do |key, value|
    hset.bind(self).call(key, value)
    end
    end

    require 'override'
    class HashUsingOverride < Hash
    override('[]='){ def []=(k,v) super end }
    end

    require "benchmark"
    def bm_report bm, title, hash_class
    hash = hash_class.new
    bm.report title do
    100_000.times do
    hash[ 1 ] = 1
    end
    end
    end

    Benchmark.bmbm do |bm|
    bm_report bm, "original", Hash
    bm_report bm, "alias", HashUsingAlias
    bm_report bm, "bind", HashUsingBind
    bm_report bm, "override", HashUsingOverride
    end



    harp:~ > ruby a.rb
    Rehearsal --------------------------------------------
    original 0.070000 0.000000 0.070000 ( 0.070856)
    alias 0.140000 0.000000 0.140000 ( 0.144095)
    bind 0.370000 0.000000 0.370000 ( 0.381127)
    override 0.470000 0.000000 0.470000 ( 0.476067)
    ----------------------------------- total: 1.050000sec

    user system total real
    original 0.070000 0.000000 0.070000 ( 0.072046)
    alias 0.150000 0.000000 0.150000 ( 0.144368)
    bind 0.390000 0.000000 0.390000 ( 0.388440)
    override 0.470000 0.000000 0.470000 ( 0.481620)



    harp:~ > cat override.rb
    class Module
    def child this = self
    @child ||= self.class.new
    @child.module_eval{ include this}
    @child
    end

    def has_child
    defined? @child and @child
    end

    def override m, &b
    this = self

    m = Module.new{
    @m = this.instance_method m
    this.module_eval{ remove_method m rescue nil }

    module_eval <<-code
    def #{ m }(*a, &b)
    um = ObjectSpace._id2ref #{ @m.object_id }
    um.bind(self).call *a, &b
    end
    code

    child.module_eval &b if b
    }

    include(m.has_child ? m.child : m)
    end
    end


    -a
     
    ara.t.howard, Dec 27, 2006
    #4
  5. That technique is fairly old (I myself tried to popularize it a few years
    ago). It's cleaner & generally safer than alias_method, but there are three
    shortcomings:
    * the environment captured by the closure is often too heavy (extra care
    needed to get rid of unwanted references is needed)
    * a method defined with define_method+block cannot take a block under 1.8
    (it's possible in 1.9, though)
    * method calls are much slower

    [1] some other examples
    http://thekode.net/ruby/techniques/CapturingMethods.html
     
    Mauricio Fernandez, Dec 27, 2006
    #5
  6. Daniel Berger

    Phrogz Guest

    To slightly hijack this thread (only slightly, since overriding methods
    is being discussed)...do any of you smart participants know the answer
    to the thread I posted Christmas eve, titled "Method equality; setting
    instance variables on Method instances"?

    http://groups.google.com/group/comp.lang.ruby/browse_frm/thread/951b2a42731ab66b

    I suspect I'm not coming up with anything better than the other methods
    discussed here, but it grates on me that I can't figure out what's
    going on.
     
    Phrogz, Dec 27, 2006
    #6
  7. Daniel Berger

    Trans Guest

    (* Trans still patiently awaits Cuts *)

    T.
     
    Trans, Dec 27, 2006
    #7
  8. Daniel Berger

    Pit Capitain Guest

    Sorry I didn't answer your original post. I waited until somebody else
    would answer it, and then I forgot it :-(

    This has been brought up several times. I'm sure Tom aka trans can tell
    you a lot about this. Look at the following IRB session:

    irb(main):001:0> Kernel.method( :puts ).object_id
    => 24666490
    irb(main):002:0> Kernel.method( :puts ).object_id
    => 24661050

    Module#method always creates a new Ruby object around the internal
    method implementation. Currently you have to cache your method objects
    in order to be able to retrieve them later.

    Regards,
    Pit
     
    Pit Capitain, Dec 27, 2006
    #8
  9. Daniel Berger

    Pit Capitain Guest

    Ara, maybe your implementation is slow, but at least you are too fast
    for me :) I just wanted to start a new thread asking for alternative
    techniques...

    Your implementation has the syntatical advantage that you can call the
    previous implementation with the super keyword.

    Regards,
    Pit
     
    Pit Capitain, Dec 27, 2006
    #9
  10. Daniel Berger

    ara.t.howard Guest

    heh - i'm working on making it stack based attm so one can do

    push_method 'foo' do
    'the new foo' or super # super works!
    end

    and, later

    pop_method 'foo' # restore super

    cheers.

    -a
     
    ara.t.howard, Dec 27, 2006
    #10
  11. Daniel Berger

    Phrogz Guest

    Ah, thanks, that explains it all. (Must have missed the earlier
    discussions about this.)
     
    Phrogz, Dec 27, 2006
    #11
  12. Daniel Berger

    Trans Guest

    I've ssen a stack used before (but I can't recall where, was it Nitro's
    aspect.rb?) Your syntax though is an interesting method-ology ;-)

    Hmm....

    defs.push 'foo' do
    ...
    end

    defs.pop

    T.
     
    Trans, Dec 27, 2006
    #12
  13. Daniel Berger

    ara.t.howard Guest

    i like that. anyone else?

    -a
     
    ara.t.howard, Dec 27, 2006
    #13
  14. Daniel Berger

    Mat Schaffer Guest

    Brain storming:

    - Wouldn't it make more sense to say defs.pop 'foo' ?
    - If I can say defs.X, then what's defs? Is it an object I can do
    other stuff with?
    - How about injecting a redefinition somewhere other than the top of
    the stack?

    I mean, the basic implementation is plenty cool. Just thought I'd
    share my crazy ideas....
    -Mat
     
    Mat Schaffer, Dec 27, 2006
    #14
  15. Daniel Berger

    ara.t.howard Guest

    yes of course.
    list of objects. probably the list of modules
    possible. but painful. you'd have to do tons of method re-shuffling to
    preserver the concept of 'super'. a bit too hard for my tastes, but i'll let
    you submit a patch! ;-)
    always a good thing imho!

    cheers.

    -a
     
    ara.t.howard, Dec 27, 2006
    #15
  16. Daniel Berger

    Paulo Köch Guest

    Ok, let me hijack this thread back to it's first subject. =P

    Considering the example, let's assume we want to redefine the method
    to encapsulte the old behaviour in a new one. Couldn't this be done
    in the metaclass[1] and the metaclass calls the object's original
    definition?

    [1] From why's article:
    class Object
    def metaclass
    class << self; return self; end;
    end
    end
     
    Paulo Köch, Dec 28, 2006
    #16
  17. How can we lighten it, if at all?
    Hm, that is a limitation, but at least the simple cases still work.
    Yes, I saw some of the benchmarks. Maybe we could memoize the binding
    somehow? I don't know if that even makes sense. I'm just tossing ideas
    out there.

    I guess for now I'll live with making my private aliases...private. :)

    Regards,

    Dan
     
    Daniel Berger, Dec 30, 2006
    #17
  18. Daniel Berger

    Trans Guest

    That's how many of these implementations work, albiet by adding a
    module to the singleton class so that more than one layer can be added
    as well.

    T=2E
     
    Trans, Dec 30, 2006
    #18
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.