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
: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
(:state)
sym.to_s.delete("?") == self.state
end
end
State.new("ready").ready? # => true
State.new("ready").reddy? # => false
# example 4
(:state)
sym.to_s.delete("?") == self.stat
end
end
State.new("ready").ready? # ~> -:12:in `method_missing': stack level too deep (SystemStackError)
# example 5
(:state)
[:ready, :running, :finished].each do |element|
define_method :" ?" 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"
["development", "test", "production"].each do |environment|
define_method :" ?" 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)
One thing I got in the habit of, regardless of language, is to put the variable second in a comparison. For example:
ReplyDeleteif "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.
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).
ReplyDeleteYou would have to make sure that this code is the last to execute in Rails loading.
Ola,
ReplyDeleteI 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
I've had this exact use-case on my mind for some time now. Glad to see it's a verified patch.
ReplyDeleteI 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.
ReplyDeleteDoes Ruby have anything similar to Perl's strictures & warnings?
ReplyDeleteMy perl knowledge is nil, but I don't believe Ruby has something similar.
ReplyDelete