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)

7 comments:

  1. Anonymous2:04 AM

    One thing I got in the habit of, regardless of language, is to put the variable second in a comparison. For example:

    if "string" == foo, or
    unless 3 == bar

    This way, if I happen to only type one =, the complier or interpreter will yell at me, rather than me pulling my hair out trying to find bugs.

    ReplyDelete
  2. Nice stuff. The last one would be good to have too, except it might fail in interesting ways when RAILS_ENV is set from within Rails (which as far as I know is done in some places).

    You would have to make sure that this code is the last to execute in Rails loading.

    ReplyDelete
  3. Anonymous10:14 AM

    Ola,
    I thought it was going to be a pain, but there are only 2 places in the rails codebase where "RAILS_ENV =" appears (initializer and test_help).

    Cheers, Jay

    ReplyDelete
  4. I've had this exact use-case on my mind for some time now. Glad to see it's a verified patch.

    ReplyDelete
  5. I also prefer class methods to constants, and not only because of the naming (elephant case sucks). Thanks to the way Rails loads constants (models, &c), you can run into some real problems with ModelName::Whatever that don't appear with ModelName.whatever.

    ReplyDelete
  6. Does Ruby have anything similar to Perl's strictures & warnings?

    ReplyDelete
  7. Anonymous3:17 PM

    My perl knowledge is nil, but I don't believe Ruby has something similar.

    ReplyDelete

Note: Only a member of this blog may post a comment.