Decorators in Ruby (migrating from Python)

jameshfisher picture jameshfisher · Dec 11, 2009 · Viewed 9.1k times · Source

I'm spending today learning Ruby from a Python perspective. One thing I have completely failed to grapple with is an equivalent of decorators. To pare things down I'm trying to replicate a trivial Python decorator:

#! /usr/bin/env python

import math

def document(f):
    def wrap(x):
        print "I am going to square", x
        f(x)
    return wrap

@document
def square(x):
    print math.pow(x, 2)

square(5)

Running this gives me:

I am going to square 5
25.0

So, I want to create a function square(x), but decorate it so it alerts me as to what it's going to square before it does it. Let's get rid of the sugar to make it more basic:

...
def square(x):
    print math.pow(x, 2)
square = document(square)
...

So, how do I replicate this in Ruby? Here's my first attempt:

#! /usr/bin/env ruby

def document(f)
    def wrap(x)
        puts "I am going to square", x
        f(x)
        end
    return wrap
    end

def square(x)
    puts x**2
    end

square = document(square)

square(5)

Running this generates:

./ruby_decorate.rb:8:in `document': wrong number of arguments (0 for 1) (ArgumentError)
    from ./ruby_decorate.rb:15:in `'

Which I guess it because parentheses aren't mandatory and it's taking my "return wrap" as an attempt to "return wrap()". I know of no way to refer to a function without calling it.

I've tried various other things, but nothing gets me far.

Answer

horseyguy picture horseyguy · Dec 12, 2009

Here's another approach that eliminates the problem with conflicts between names of aliased methods (NOTE my other solution using modules for decoration is a good alternative too as it also avoids conflicts):

module Documenter
    def document(func_name)   
        old_method = instance_method(func_name) 

        define_method(func_name) do |*args|   
            puts "about to call #{func_name}(#{args.join(', ')})"  
            old_method.bind(self).call(*args)  
        end
    end
end

The above code works because the old_method local variable is kept alive in the new 'hello' method by fact of define_method block being a closure.