Saturday 3 November 2007

Advanced Ruby Tips And Tricks, Part 1

Welcome to this first installment of Advanced Ruby Tips and Tricks. In this little series I plan on covering anything ruby which goes beyond writing basic classes and methods; topics like meta-programming, runtime callbacks, closures and continuations. Doubtless for some of you none of these topics will seem particularly advanced, but that's because you're already advanced :) So here goes...

Problem


You are designing an API. You want to include some small class which has dependencies the rest of your library does not have, and whose use is only optional, but you don't want the end user to have to require the files manually.

Solution

class XMLTracer

def self.new
puts "loading..."
require 'rubygems'
require 'builder'
rescue LoadError
raise LoadError, "must have builder installed..."
else
def self.new
puts "new is the new new"
super
end
new
end

def initialize
@xml = Builder::XmlMarkup.new(
:target => STDOUT,
:indent => 2
)
end

def identity
@xml.instruct!
@xml.comment! "Crepuscular Homunculus :: " +
"Advanced Ruby Tips and Tricks, Part 1"
@xml.class do |c|
c.name self.class.name
c.id object_id
end
end

end

x1 = XMLTracer.new
x2 = XMLTracer.new
puts
x1.identity
x2.identity
Which Outputs

loading...
new is the new new
new is the new new

<?xml version="1.0" encoding="UTF-8"?>
<!-- Crepuscular Homunculus :: Advanced Ruby Tips and Tricks, Part 1 -->
<class>
<name>XMLTracer</name>
<id>-607742088</id>
</class>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Crepuscular Homunculus :: Advanced Ruby Tips and Tricks, Part 1 -->
<class>
<name>XMLTracer</name>
<id>-607742198</id>
</class>


Walkthrough



The first odd thing you might notice, which you don't see in the wild that often, is that the body of our self.new method contains a rescue and else clause. This is possible because method bodies implicitly are begin end blocks.

The other slightly odd thing if you're coming from more static languages is that the require statements are actually just methods and so can be called dynamically at runtime. What we are doing here is calling require only if the end-user creates a new instance of our XMLTracer class. If for some reason the requires fail, we'll catch and rethrow the load error.


def self.new
require 'rubygems'
require 'builder'
rescue LoadError
raise LoadError, "must have builder installed, blah blah blah"


Here is where it starts to get interesting. The code after the else clause is only run if no exceptions are thrown, so we know that builder has been successfully loaded. At this point we could just call super because require is smart enough not to reload previously loaded files, but that represents some unnecessary overhead every time a new XMLTracer is instantiated.


else
def self.new
puts "new is the new new"
super
end
new
end


Ruby's dynamic nature means that methods can be re-bound at runtime, so we redefine the new method to just call super, and then we call it explicitly to return a new instance. But from then on calling XMLTracer.new will not attempt to require anything. Which you can see in the output, where "loading..." is written only once.

Well, that wraps up this first installment of ARTT. Hope it has been of some use to you. If you have any suggestions/requests for future articles, please let them be known through the comments.

cheers, and happy hacking!

2 comments:

Anonymous said...

Good stuff - please continue the series!

Anonymous said...

People should read this.