In 2006 I was introduced to ruby, and for a time I found it to be a fun language to work with. After about 6 months, I started to notice problems and shortcomings in the ruby/rails stack and went on to bigger and better things. The problem is that to this day I have to defend myself to legions of ruby/rails faithful who seem to think that it’s sacrilege to ever leave the faith. (That makes me what, an apostate?) A friend recently asked on a mailing list I regularly traffic why a person would consider ruby to be flawed. The best way I can express this is in some code.

As a note, this isn’t about these specific flaws: It’s that these kinds of flaws are endemic in ruby and particularly rails.

Set Processing

devon@chronos ~/Projects/sets $ cat sets1.rb
#!/usr/bin/env ruby
require 'set'
require 'pp'
large = Set.new(Range.new(1,10000000))
small = Set.new(Range.new(1,10))
pp small.intersection(large)

devon@chronos ~/Projects/sets $ time ./sets1.rb
#<Set: {5, 6, 1, 7, 2, 8, 3, 9, 4, 10}>

real 0m35.421s
user 0m23.900s
sys 0m9.910s


devon@chronos ~/Projects/sets $ cat sets2.rb
#!/usr/bin/env ruby
require 'set'
require 'pp'
large = Set.new(Range.new(1,10000000))
small = Set.new(Range.new(1,10))
pp large.intersection(small)

devon@chronos ~/Projects/sets $ time ./sets2.rb
#<Set: {5, 6, 1, 7, 2, 8, 3, 9, 4, 10}%gt;

real 0m16.917s
user 0m12.720s
sys 0m3.430s

So the order that I intersect the sets drastically impacts the speed of the algorithm (2x). Let’s take a look at python to see whether a language in the same class behaves similarly.


devon@chronos ~/Projects/sets $ cat sets1.py
#!/usr/bin/env python

large = set(range(1,10000000))
small = set(range(1,10))

print large.intersection(small)

devon@chronos ~/Projects/sets $ time ./sets1.py
set([1, 2, 3, 4, 5, 6, 7, 8, 9])

real 0m2.583s
user 0m1.850s
sys 0m0.670s


devon@chronos ~/Projects/sets $ cat sets2.py
#!/usr/bin/env python

large = set(range(1,10000000))
small = set(range(1,10))

print large.intersection(small)

devon@chronos ~/Projects/sets $ time ./sets2.py
set([1, 2, 3, 4, 5, 6, 7, 8, 9])

real 0m2.598s
user 0m1.760s
sys 0m0.710s

So no, python handles the choice of which set to intersect with the other gracefully. This is a good example of how the ruby set api is immature. I’m ignoring that it’s taking between 13.7 and 6.5 times as long to execute, because I think that it’s reasonable for people to do code in something that is easier for them to work in and then optimize. My problem here is that based on the direction you do the intersection, it can take 2x longer in ruby. This is shoddy engineering and is the kind of thing that gets solved in a language as it matures. The problem is that the language was immature when it exploded into widespread use, and no one seems to be doing anything to make it ready for prime time.

So what about 1.9? 1.9 is supposed to be where the energy is going to make the language faster and more mature.

devon@chronos ~/Projects/sets $ cat sets1.rb
#!/usr/bin/env ruby1.9
require 'set'
require 'pp'
large = Set.new(Range.new(1,10000000))
small = Set.new(Range.new(1,10))
pp small.intersection(large)

devon@chronos ~/Projects/sets $ time ./sets1.rb
#<Set: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}>

real 0m13.310s
user 0m11.240s
sys 0m0.680s


devon@chronos ~/Projects/sets $ cat sets2.rb
#!/usr/bin/env ruby1.9
require 'set'
require 'pp'
large = Set.new(Range.new(1,10000000))
small = Set.new(Range.new(1,10))
pp large.intersection(small)

devon@chronos ~/Projects/sets $ time ./sets2.rb
#<Set: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}>

real 0m9.181s
user 0m8.280s
sys 0m0.480s

While it’s faster (which wasn’t really my complaint anyway), it still is very different based on which direction you do your intersection from. I see evidence of speed work being done on ruby, but not on maturing the language so that it meets tried and true engineering principals like the rule of least surprise. (Yes, code is a user interface for coders, so the rule of least surprise applies) (1)

Nil as an object
How about this one that has actually done damage to our database:


devon@chronos ~/Projects/sets $ irb
irb(main):001:0> nil.id
(irb):1: warning: Object#id will be deprecated; use Object#object_id
=> 4

It certainly explains why we have a bizarrely large number of items in our database with foreign keys set to 4. This conflicts directly with the rails standard for naming a database primary key as ‘id’, which you assess on a model at obj.id. Yes, code should have lots of null checks, but real world code has flaws. Sure it throws a warning, but since it doesn’t break on nil when you call .id, you get data corruption. Gee, thanks, ruby.

Ruby in general
These are just a few examples, but ruby is rife with happy path coding. The language needed another 5 years of development before going into widespread use. Instead, I think it’s getting worse because a set of rails engineers who don’t know anything about languages are at the helm, adding lots of syntactic sugar but with a distinct lack of substance behind that candied shell.

1. Perhaps it’s not fair to compare ruby to the unix philosophy, because the ruby design philosophy violates the 17 rules in ways that are clearly intentional. Specifically, ruby and rails egregiously violate the rules of clarity and transparency, and rails in particular violates the rules of separation, composition and representation.