2 min read

Python Decorators in Ruby

This past week, I had a pretty long discussion on StackOverflow about Ruby and Python, that was touched off by question about whether there was anything special about Ruby that made it more amenable for Rails.

My initial response covered blocks (as a self-hosting way to improve the language) and the ability to define new "class keywords" because of the fact that class bodies are just normal executable code. The strongest argument against this was that Python decorators are as powerful as Ruby's executable class bodies, and therefore can be used to achieve anything that Ruby can achieve.

Python decorators do in fact provide a certain amount of additional power and elegance over the Python before decorators. However, this power is simply a subset of the available Ruby functionality. This is because Ruby has always had the ability to add features like decorators to the language without needing language changes.

In other words, while Python has added a new hardcoded feature in order to achieve some of the power of Ruby, adding such a feature to Ruby would be unnecessary because of the underlying structure of the Ruby language.

To show this clearly, I have implemented a port of Python function decorators in under 70 lines of Ruby. I wrote the code test-first, porting the necessary functionality from two Python Decorator tutorials written by Bruce Eckel in October 2008.

The list of features supported:

  • Decorate any object that responds to call via: decorate SomeKlass or decorate some_lambda
  • A simpler syntax for class names that can be used as decorators: SomeKlass(arg)
  • The ability to give the decorator a name that could be used instead of the class name, if desired

Some examples:

class MyDecorator
  def initialize(klass, method)
    @method = method
  end
  
  def call(this, *args)
    puts "Before MyDecorator with #{args.inspect}"
    @method.bind(this).call(*args)
    puts "After MyDecorator with #{args.inspect}"
  end
end

class WithDecorator
  # we could trivially add these to all classes, but we'll keep it isolated
  extend MethodDecorators

  MyDecorator() # equivalent to decorate MyDecorator
  def my_function(*args)
    puts "Inside my_function with #{args.inspect}"
  end
end

WithDecorator.new.my_function(1)

When MyDecorator() is called inside the WithDecorator class, it registers a decorator for the next method that is defined. This is roughly equivalent to Python's function decoration. Some other examples:

class MyDecorator < Decorator
  decorator_name :surround

  def initialize(klass, method, *args)
    @method, @args = method, args
  end

  def call(this, *args)
    puts "Before MyDecorator(#{@args.inspect}) with #{args.inspect}"
    @method.bind(this).call(*args)
    puts "After MyDecorator(#{@args.inspect}) with #{args.inspect}"
  end
end

class Class
  include MethodDecorators
end

class WithDecorator
  surround(1, 2)
  def function(*args)
    p args
  end
end

WithDecorator.new.function(1)

In this case, I've inherited from the Decorator class, which allows me to add a decorator name, which can then be used in a class with MethodDecorators mixed in. In this example, I've mixed MethodDecorators into Class, which makes decorators available for all classes. Again, I could have made this the default, but I like to try to make new behaviors global if I can avoid it.

This is of course a first pass and I'm sure there are subtle inconsistencies between Python's decorator implementation and what I have here. My point is just that the feature that was added to Python to add flexibility is merely a subset of the functionality available to Ruby, because Rubyists can implement new declarative features without needing help from the language implementors, and have always been able to.