Monday, July 14, 2008

Ruby programming is art

I realize that the Ruby language is special, in the sense that my learning curve is experienced differently than it was with Java, or even C++ back then. Some steps take a different amount of time, some others are of another nature. Specifically, I find that beyond learning the basics in Ruby (which is arguably quicker than the same in Java), the programmer's creativity is much more important in Ruby than it is in Java.

Let me take a very simple example. Let's say you need to write code that needs to trigger something when a post shows up on a specific blog. Here are 2 approaches to this simple programming drift.

The naive approach

You need the job done, you go the procedural way. It's quick, and it works.


#!/usr/bin/env ruby



require 'open-uri'

require "rexml/document"

require 'rexml/xpath'



feed_link = "http://feeds.feedburner.com/37signals/beMH"

title, published_date = "", ""

TITLE_PATH = "//item//title"

DATE_PATH = "//item/pubDate"



while true do

  open(feed_link) do |feed|

    doc = REXML::Document.new feed

    fetched_title = REXML::XPath.first( doc, TITLE_PATH ).text

    fetched_published_date = REXML::XPath.first( doc, DATE_PATH ).text



    if title != fetched_title

      puts "#{fetched_title} (#{fetched_published_date})"

      system 'xclock' #trigger something

      title = fetched_title

      published_date = fetched_published_date

    end

  end



  #wait 5 minutes

  sleep 5*60

end




The Ruby-way approach

The naive approach worked, but it does not produce Ruby code, to be a tad nagging. The Ruby-way brings proper abstractions in the picture; a sense of high-level. Props to Mats for the advice on that one.


#!/usr/bin/env ruby



require 'rubygems'

require 'open-uri'

require 'feed-normalizer'



def every seconds, &block

  while true

    yield

    sleep seconds

  end

end



class Fixnum

  def minutes

    self * 60

  end

end



class Feed

  def initialize url

    @url = url

    @title = nil

  end



  def has_recent_post? &trigger

    feed = FeedNormalizer::FeedNormalizer.parse open(@url)

    fetched_entry = *feed.entries.first

    title = fetched_entry.title

    published_date = fetched_entry.last_updated



    if title != @title

      puts "#{title} (#{published_date})"

      @title = title

      trigger.call

    end

  end

end



#high-level functionality

feed = Feed.new "http://feeds.feedburner.com/37signals/beMH"

every 5.minutes do

  feed.has_recent_post? do

    `xclock` #trigger something

  end

end




Nicer! You see the style? There are a few changes, and most of them are for the better.

Observations and impacts
  • every method as a top-level construct provides for an interesting abstraction
  • monkey patching of Fixnum adds code readability
  • the Feed class brings a useful model
  • FeedNormalizer gem removes the feed parsing logic
  • FeedNormalizer gem supports both RSS and Atom
  • the high-level functionality at the end reads like English
  • more LOC is a minus (although every, Fixnum and Feed could be reused elsewhere)

The learning beyond the basics

With Java, once you learned the basics (for example via a Sun Certified Java Programmer certification), you're pretty much all set with the language.

With Ruby, after you learned the basics (for example via the PickAxe), you're not done with the language. You need to become familiar with the idioms and discover all the sugar syntax. You need to acquire the Ruby-way.

And of course you will improve your programming skills by being receptive to best practices and patterns, but that's common to both Ruby and Java.


The art

Acquiring the Ruby-way takes time. It's not only about using OO principles. It's also about whether or not you should use monkey patching, choosing the proper abstraction (a top-level method, a module, or a class?), devising elegant closures, as well as what I could call phrasing your code (when is it enough English-like?). It feels like you have important choices to make, which requires responsibility and creativity. Mastering the basics, although necessary, is not enough.

You also need to develop your style. I would dare to say that programming style in Java is enforced by the company code standards, whereas programming style in Ruby comes from the personality and the programming experience.

Ruby programming is art. And it's part of the reasons why it's fun.

9 comments:

Aaron Müller said...

Great Example!
Thanks!

Greg said...

Keep it local:

def minutes = 5
every (5 * minutes) do

Equivalent syntax, no complications.

Yeah Ruby definitely has more flexibility than Java in a lot of ways, and it takes time to learn the best way of doing a lot of things once you have the choice. Unfortunately most don't take the time. :(

Oshuma said...

An excellent example in the 'proper' use of Ruby. Great post!

Daicoden said...

What is your opinion on libraries patching classes? For example if the FeedNormalizer gem modified minutes then by redefining it you just caused an inadvertent bug that is not easily detectable. I definitely like the ruby better in the higher level version, but I recently had a problem where two libraries conflicted! What do you think we can do to avoid that in the future?

If things keep going the way they are where every big library starts making their own version of the base classes I think we will get to a point when updating gems will cause errors in others and even though the higher level code is readable development time will be spent going through and hacking the libraries to work together which is much tougher then the procedural method.

Olafski said...

@Greg:
There are a few things wrong with that:
- def defines a method, don't you mean minutes = 5?
- 5 * minutes would be 5 * 5 = 25 :)
- rails has a minutes method, so fixing the code would be something like
minutes = 5
every minutes.minutes do

Doesn't sound like the correct approach to me :s

fred said...

This is an absolutely beautiful code.

Excellent :)

Martin Carel said...

@all: Thanks for the good words.

@daicoden: you had me write a post about monkey-patching. You should thus have some of my personal insights on the issue!

Peter Marklund said...

Hey Martin!
Very nice article! Great to see that you are on your way to becoming a Ruby ninja! Java free for how long now? :-)

Let me know if you are back in Stockholm again. Would be nice to hook up. I have a goal to make it to Canada one day too...

Cheers

Peter

Ely said...

I just have two questions:

1. Why do you use:
fetched_entry = *feed.entries.first
instead of
fetched_entry = feed.entries.first

2. Why do you use:
trigger.call
instead of
yield

as you used in the minutes method.

Thank yo