A post that’s been making the rounds lately, it seems, is Tim Bray’s “On Ruby”. Bray praises Ruby as being “remarkably, perhaps irresistibly, attractive,” especially for people (like Bray) who are proficient in Perl and Java. As some see Bray’s post as a pro-dynamic-language post as much as (or more than) a pro-Ruby post, I finally decided to take a read.
As I was reading, this paragraph jumped out at me:
Maybe the single biggest advantage is readability. Once you’ve got over the hump of the block/yield idiom, I find that a chunk of Ruby code shouts its meaning out louder and clearer than any other language. Anything that increases maintainability is a pearl beyond price.
While I’ve still done very little actual coding in Ruby, I have found the above to be true with most code that I’ve come across. I love reading source. Well, perhaps read is not the right term. I love to look at it, just as I love to look at screen shots when reading about any particular piece of desktop software. I have a deep love for design, and much of my own personal artwork is grounded more in design aesthetics than conventional art aesthetics, so I often take a visual approach to code. And what appeals to me at any given moment often shifts.
Ruby code tends to be quite attractive to me these days. I’m not sure why, entirely. But I think Bray hits it on the head in his next paragraph. Funny thing: when I was reading the paragraph quoted above - regarding readability - the thought “hmm, what about Python?” ran through my head. That, too, is answered in the following.
In theory, Python ought to do better, lacking all those silly end statements cluttering up the screen. But in practice, when I look at Python code I find my eyes distracted by a barrage of underscores and double-quote marks. Typography is an important component of human communication, and Ruby’s, on balance, is cleaner. Tim Bray, “On Ruby”, Jul 24 2006
Typography. Could that really be it? You know, he may be right. Phillip Eby recently had a post about Python Based DSLs. He brings up a couple of the elements that seem to give Ruby the power to make nice little domain-specific-languages in, which projects like Rails use to great advantage.
However, Ruby’s advantage in this area basically boils down to two things: being able to apply functions without parentheses, and having code blocks. The first is nice because it makes it possible to create commands or pseudo-statements, and the second is a necessity because it allows those pseudo-statements to encompass code blocks. Without at least the second of these features, Python is never going to be suitable for heavy-duty DSL construction. Phillip J Eby, “Schema Analysis and the need for Python-based DSLs”, Jul 15 2006
I think Eby captures a couple of the more common elements that make this work for Ruby. However, I think there are a couple of other elements that make Ruby look particularly good in this area. The first is Ruby’s use of Symbols. One description I found for Symbols (I forget the source) is that they’re like Strings that you’ll never show to the user. Therefor they don’t need to be printed. They also seem to work with the Ruby’s equivalent (as far as I understand it) of Python’s identity comparison (in python: obj is None
, foo is marker
). I’d always seen Symbols in Ruby code, but it was Rails that really made me take notice, as they’re used all over the place. Symbols in Ruby are those things that begin with a colon:
finished = Todo.find :all, :include => [:something]
render :action => 'error'
There is something about that that just looks kindof… nice. :all
. In my editor, Symbols are colorized differently than strings, which helps them stand out even more. find :all
. Not findAll()
or find(all=True)
or find('all')
like one might have in Python (all of which are OK solutions, but man.. those Symbols).
Now as much as the blocks, Symbols, and optional parenthesis seems to help Ruby - it’s the meta-programming that really seems to stand out. This is some ActiveRecord code from Tracks, a GTD-focused task management application written in Rails:
class Project < ActiveRecord::Base
has_many :todos, :dependent => true
has_many :notes, :dependent => true, :order => "created_at DESC"
belongs_to :user
acts_as_list :scope => :user
attr_protected :user
# Project name must not be empty
# and must be less than 255 bytes
validates_presence_of :name, :message => "project must have a name"
validates_length_of :name, :maximum => 255, :message => "project name must be less than %d"
validates_uniqueness_of :name, :message => "already exists", :scope =>"user_id"
...
These are statements in the class level that read… easily. A project has many todos and notes and belongs to a user. It must have a unique name for an individual user that is less than 255 characters. Wow. That is a lot of information in a small space. And you don’t think of has_many
as a function or method or something that’s pulling of crazy metaclass trickery: it reads like a statement. I see this done so often in Ruby that writing things like has_many
are not that difficult (I’m not talking about the implementation behind it, since O-R mapping is always a beast; I’m talking about how easy it is to operate on the Project
class in-line like that).
Now this is doable in Python. Zope 3 has applied similar things, albeit sparingly (and with good cause). You can’t really monkey with the Project
class in Python while inside the class
statement without resorting (at the least) to execution frame tricks. But one you’re comfortable with them - you can start doing some nice things. In the early days of Zope’s Interface system, declaring support for what was implemented by class instances looked like this:
class Project(Base):
__implements__ = IProject
__used_for__ = IUser
Now it can be this:
class Project(Base):
implements(IProject)
adapts(IUser)
In a SQLAlchemy based system I’ve worked on for a major project I’m involved with, I’ve applied some of these techniques to yield code like this:
class FeeScale(Base):
act.baseTable(
fee_scale, classify='type',
order_by=fee_scale.c.price_floor,
)
@classmethod
def getFeeForPrice(cls, price, owner, default=None):
fee = cls.selectFirst(
(cls.c.price_floor < price)
& (price <= cls.c.price_ceil)
& (cls.c.owner_id == owner.id)
)
if fee:
return fee.service_fee
return default
class RetailerFeeScale(FeeScale):
act.whenClassified('retailer')
implements(interfaces.IRetailerFeeScale)
classProvides(interfaces.IQueryableFeeScale)
owner = props.One(ILinkedFee['owner'])
class WholesalerFeeScale(FeeScale):
act.whenClassified('wholesaler')
implements(interfaces.IWholesalerFeeScale)
classProvides(interfaces.IQueryableFeeScale)
owner = props.One(ILinkedFee['owner'])
It doesn’t read quite as nice as the Ruby example above, and it’s from a different problem domain. But it’s not bad… I had to move my validation and relationship-related items into Zope Schema fields which are about specifying design and contracts. Since I use them heavily in order to do things like form generation, ILinkedFee['owner']
contains the information about the relationship, props.One
is a Python descriptor that uses that field information in conjunction with SQLAlchemy’s properties to handle relationships between two mapped objects.
class ILinkedFee(Interface):
owner = schema.HasOne(
title=u"Owner",
model=Ref.relative('.base.Owner'),
options=dict(lazy=True),
)
There are upsides and downsides to this setup. One really big downside is that there’s no single place to see everything going on with RetailerFeeScale
. Earlier versions of this code had functions that played tricks inside of a class suite to spell things like hasOne(...)
without assignment or using a second mechanism such as Interface schemas. That code, however, was playing too many tricks and causing too much trouble. And this setup works well with Zope 3.2 and the latticework I’ve put in place to support it.
But it seems like a lot of work, learning metaclasses and descriptors and decorators and getframe() trickery. However, looking at this mammoth project’s code tonight, I’m impressed by how concise and readable most of it is. Ruby and Smalltalk have both been major influences on me lately, even if I seldom get to actually use those languages directly. God I wish for blocks though. I think that the with
statement in Python 2.5 will help. A place where Blocks seem especially powerful is their ability to construct an object, yield it to the block, let the block do things with it (set attributes, etc), and then the thing which yielded control can do more things with the object. Hmm. That’s vague. Here’s one case: explicitly saving (flushing) a SQL Alchemy object after doing a bunch of updates. It’s one of those lines that bugs me: it’s something important (it should happen), but it’s not necessarily important to the business logic. It’s small, and it often blends in with its surroundings:
fee = Fee.get(1)
fee.floor = 0.0
fee.ceil = 10.0
fee.price = 1.25
fee.save()
Small annoyance, really. But it does bug me. I believe that the with
statement might help out, ensuring that the save happens immediately:
with savable(Fee.get(1)) as fee:
fee.floor = 0.0
fee.ceil = 10.0
fee.price = 1.25
And with that, I put to bed this evening’s incredibly long and pointless rambling, whose beginnings I scarcely remember. Oh yeah. Ruby’s punctuation and typography is beautiful. It really is. And I envy it.