Thursday, December 20, 2007

Avoiding costly typos

Typos are generally unfortunate, but greatly upsetting when they cost you a few hours of your life. I have a few rules I try to follow in an attempt to conserve those valuable hours in the future.

Ruby has symbols. I love symbols. But, using symbols for comparison is an easy way for a typo to cost you time.

# example 1
name = :shane
name == :shane # => true
name == :chane # => false

# example 2
class Name
def self.shane
:shane
end
end

name = Name.shane
name == Name.shane # => true
name == Name.chane # => undefined method `chane' for Name:Class (NoMethodError)

Above, in example one, a typo simply returns false. However, in example two the typo gives me the immediate feedback that I made a mistake. You could argue that I should simply use a constant. Elephant case (All upper case) words bother me for some reason, but if you prefer constants that's cool too, you'll get the same benefit.

The method_missing method is dynamite. Used appropriately it's a powerful tool. However, there are often times when you simply don't need dynamite.

# example 3
class State < Struct.new(:state)
def method_missing(sym, *args)
sym.to_s.delete("?") == self.state
end
end

State.new("ready").ready? # => true
State.new("ready").reddy? # => false

# example 4
class State < Struct.new(:state)
def method_missing(sym, *args)
sym.to_s.delete("?") == self.stat
end
end

State.new("ready").ready? # ~> -:12:in `method_missing': stack level too deep (SystemStackError)

# example 5
class State < Struct.new(:state)
[:ready, :running, :finished].each do |element|
define_method :"#{element}?" do
self.state == element.to_s
end
end
end

State.new("ready").ready? # => true
State.new("ready").reddy? # ~> -:10: undefined method `reddy?' for #<struct State state="ready"> (NoMethodError)


Examples three and four illustrate the two common typos that can cost you time while utilizing method_missing. Example five shows an alternative that requires slightly more code, but is significantly better at letting you know when you've made a mistake.

If right now you are thinking "that's nice, but I write tests so I'll catch it there" then you get points for writing tests, but you missed one important note: If the typo is in your test you could be getting a false positive. I once found a bug where the same typo existed in a class and the test for the class. The result was broken production code and a green test suite.

The last tip builds on the first two: Don't use strings for comparison. As an alternative to using constants or class methods, you could define methods on a string (or a symbol) to query for the value.

RAILS_ENV = "development"
class << RAILS_ENV
["development", "test", "production"].each do |environment|
define_method :"#{environment}?" do
self == environment
end
end
end

RAILS_ENV.test? # => false
RAILS_ENV.development? # => true
RAILS_ENV.developmant? # ~> -:12: undefined method `developmant?' for "development":String (NoMethodError)

The last example is nice because it allows you to type less when doing a comparison and provides you better feedback if you do make a typo. (drop a +1 on this ticket if you want this feature in Rails core: http://dev.rubyonrails.org/ticket/10583)
Post a Comment