Search code examples
pythonrubydecorator

Decorators in Ruby (migrating from Python)


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 `<main>'

Which I guess is 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.


Solution

  • 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.