I’ve spent the last 60 days learning Ruby, laboring almost full time on a soon-to-be-revealed project. I’ve worked professionally with many different languages, and I figured it was about time to summarize my thoughts. I will discuss Rails in a future post.
Let’s start with some things I love about Ruby.
I Love Ruby. Really.
Goodbye Perl and Python, We Hardly Knew Ye
Ruby is going to completely destroy Perl and Python. We will have forgotten all about them in a few years.
Perl is a fun little language but I can’t see why a neutral developer would ever choose to use it over Ruby. Everything that I liked about Perl can be found in Ruby, including easy regular expressions, rich interpolation, and end of line conditionals. Ruby is a modern programming language and it shines a harsh light on Perl, exposing the glaring cracks that have opened up during its long devolution. Where to begin? Threading. Objects. Creaky syntax. At this point, the only Perl feature I miss is CPAN. Ruby gems is a joke by comparison.
Python is a more recent, robust language than Perl, but it too will quickly succumb to Ruby’s onslaught. Python preaches whereas Ruby tries to be your friend. Ruby is just more fun. Python never really achieved widespread adoption, and I think that most neutral people will choose to learn Ruby instead.
I have deep knowledge of Perl and some experience with Python. If you’re considering learning either of these scripting languages for your project, I recommend using Ruby instead.
A Sweet Tooth For Syntactic Sugar
I am a huge fan of syntactic sugar. For those who are unfamiliar with the term, “syntactic sugar” refers to syntax features specifically added to make it easier to write code. Here are some examples of syntactic sugar in Ruby:
| Old school |
With sugar added |
if a == nil
a = 3
end
|
a ||= 3
|
if !a.saved?
a.save
end
|
a.save if !a.saved?
|
Syntactic sugar makes it easier to “eyeball” a block of code and results in a big win for productivity. Ruby is littered with sugary goodness, which makes it a very productive language indeed. I can state this definitively after my first sixty days.
Ruby “Blocks” Make Engineering Easier
Ruby makes it possible to easily hand a “block” of code to a function. Some simple examples:
# add one to every element in an array
a.map { |i| i + 1 }
# replace words with definitions in a string
string.gsub(/\\b\s+\\b/) { |i| definitions[i] }
# open a file, write "hello", close the file
File.open("tst.txt", "w") { |f| f.puts "hello" }
Most of these aren’t very compelling, and there is always an easy way to write the code without using blocks. But I keep running into patterns that really turn out well with blocks. For example, I recently had to write some code that cached database records for performance reasons. Here’s what that code looks like without blocks:
def initialize
cache = Hash.new
end
def get_from_cache(key)
value = cache[key]
if value == nil
value = cache[key] = get_from_db(key)
end
value
end
Eventually I discovered that when you create a Hash object you can pass a block to tell the Hash how to populate uninitialized values. So the above code becomes:
def initialize
cache = Hash.new { |hash,key| hash[key] = get_from_db(key) }
end
def get_from_cache(key)
cache[key]
end
I keep using this pattern over and over and each time I appreciate blocks a little more. Blocks make it easier to write excellent software.
“It’s Just a Hashtable”
In a previous post (The 6,000 Line Hashtable) I talked at great length about why developers should be lazy and use hashtables at every opportunity. In Ruby, hashtables are supported syntactically just like arrays:
array = [1, 2, 3]
array[0] = 'hello'
hash = {'a' =>'b', 'c' => 'd'}
hash['a'] = 'world'
This feature isn’t unique to Ruby but it’s still worth noting.
In other languages (c/c++/java/etc.), developers sometimes unnecessarily create elaborate data structures and classes where a hashtable (or a hash of hashes) would just as easily accomplish the same task. People often think that hashtables are too slow or too expensive for their purposes. When hashtables are built into the language, it’s more difficult to harbor those misconceptions.
Stop Writing Bash Scripts. I’m Begging You.
Professional developers often resort to shell scripts for mundane tasks like builds, deployment, database cleanup, automated backups, and a million other secondary concerns. I don’t like bash scripting. I don’t use it often enough to completely master the syntax. I can barely remember how to write an “if” statement in bash, let alone a loop or a switch. Yet I’ve written hundreds of bash scripts simply because there wasn’t a better tool for the job. Those scripts were difficult to write and I doubt anyone bothered to maintain them after I abandoned them.
Since I started learning Ruby, I stopped writing bash scripts. Cold turkey. Everywhere that I might be tempted to write a bash script, I simply use Ruby instead. I wrote a 300 line deployment script for remotely setting up a machine at ServerBeach, all in Ruby. My cron jobs are Ruby. My db setup scripts are Ruby. With luck I won’t have to cobble together another bash script anytime soon.
I’m not religious about languages, I just find it easier to write short Ruby scripts instead of bash scripts.
Please, don’t reply and tell me that ruby is too heavyweight to replace bash. I agree that this is true for some specific tasks, tasks which most developers are unlikely to encounter.
Now, The Rough Spots
There are some things in Ruby that are immensely frustrating. The language has only been around for a few years and I’m not surprised there are rough spots. Here are a few areas that need improvement.
The Man Behind the Curtain is Terrifying
Ruby’s dynamic type system encourages developers to engage in all kinds of neato tricks. “Gee, I can add a method to the String class and use it everywhere!” “Gosh, instead of defining my methods up front I can override method_missing to dynamically add them on the fly!”
If you’re using one of these amazing libraries, wonderful things happen behind the curtain. Your object is magically talking to a database without any intervention on your part. Useful member variables appear when you need them. You can create special helper classes that (somehow) are instantaneously available throughout your entire product without having to use a single “requires”.
That’s all fine and dandy during your first 30 days with Ruby. Unfortunately, this review looks back at 60 days.
What happens when something goes wrong? Good luck trying to figure out what the hell is happening behind that lovely curtain. It’s hard to trace the runtime behavior because so much code is dynamically, inscrutably generated. If you read the library source you’ll find that because Ruby supports mixin classes, seemingly simple APIs are splattered into a dozen files. One class can magically insert itself into another with the greatest of ease.
So, how do I track down problems? Grep. I grep the whole “gems” tree to figure out why things are happening. When grep fails, I use a more powerful grep. Google. When Google fails, I start adding printfs to the support libraries. If I still can’t figure out which class is responsible for the poor behavior, I start cutting features.
Obscure Operators Considered Harmful
I was delighted to discover that Ruby has and and or operators that can be used in place of the traditional && and ||. I lovingly sprinkled them throughout my code because the readability was so much better.
Unfortunately, Ruby’s highly readable boolean operators have a subtly different precedence when compared to the traditional operators. Simple code which looked like it should work failed for inexplicable reasons. I spent hours tracking down one problem after another, continually thinking that somehow my code was at fault. Instead of lovingly sprinkling readable boolean operators throughout my code, I was unknowingly sprinkling bugs. Ticking time bombs. Turds.
This made me very angry.
For added frustration, try running this block of code:
a = b = 2
a++
b *= 7
puts a, b
The above parses and runs just great, except that Ruby doesn’t support the ++ operator and your code won’t work at all! This kind of thing can easily be caught by turning on warnings, but you really don’t want to do that if you’re using gems because you’ll drown under an avalanche of warnings that you can’t fix and can’t suppress.
Standard Libraries Need a Plunger
This has been well documented elsewhere so I won’t go into overwhelming detail. The standard libraries are clogged up with cruft and are severely in need of a plunger. In my opinion the most basic classes should also be the simplest.
For example, I shudder every time I have to use the IO/File classes. As the Ruby doc dryly states, “The two classes are closely associated.” An alternative approach, and in my opinion a superior one, can be seen in Java’s wildly successful layered stream API.
Despite my complaining, I don’t think this is a fatal flaw. But combined with my next point this kind of clog can fill your entire product up with sewage.
Take RDoc Out Behind the Woodshed
RDoc is Ruby’s tool for generating documentation based on comments embedded in source. It’s conceptually similar to Javadoc, Doxygen, and many others. Here is an example of an RDoc page:
http://www.ruby-doc.org/core/
I really can’t understand how RDoc can be so bad. It just plain sucks. The pages are impossible to decipher. I feel pity for anyone trying to read an RDoc page using IE, which lacks Firefox’s find-as-you-type feature.
To add insult to injury, due to Ruby’s mixin madness it’s often impossible to even see a full list of supported methods for a class. It took me two weeks just to figure out how File.readlines() worked, even though I’ve seen it used countless times in sample code. Same thing goes for Hash.grep().
This is a terrible shame, because most of the important methods are actually well documented, complete with examples and grouping of similar methods. The Rails documentation is excellent, if you can stomach RDoc’s spaghetti long enough to locate it.
So, what do you get when you combine clogged standard libraries with an atrocious set of generated documentation? A bloody mess. Around day 45 I managed to get Ruby’s ri tool working on Ubuntu. Otherwise our product would still be languishing on the drawing board.
I Must Be an Idiot, ‘Cause I Don’t Get Modules
I saved this one for last because, well… I’m embarrassed. I am a professional software developer. I take pride in my work. Is it possible that I’m an idiot?
I’ve tried to write modules, I really have. I want to mixin functionality just like the popular libraries! I want my amazing caching class to magically get used everywhere! To date, all my pathetic attempts have resulted in abject failure.
I can’t figure out extend vs. include. I can’t seem to successfully call define_method on anything. My static members are rarely accessible when I expect them to be. extend self gives me the willies, and singletons give me nightmares. Sometimes self is an object, other times it appears to be something completely different. I managed to dynamically add a member to an object once, but I think I just got lucky.
At a certain point during my 60 days, I stopped blaming myself and started asking around. Am I the only one with this problem? Do any of my friends understand this voodoo? I turned to google for answers and learned that “extend self is cool because less typing is good”.
I’m not really interested in fancy language tricks. I just want to create useful software. Please, Ruby, stop trying to be cool and just focus on being easy to use, well documented, and lovable. We can talk about performance when you’re a little older.