|
RubyIdioms
Ruby conventions and idioms
This document show some Ruby idioms, conventions and patterns (most of them accepted by the community, some of them more personal). This is a work in progress, feel free to email me add a comment if you have any suggestion (tokland AT gmail.com).
IdiomsGeneral formattingChoose a sound formatting style (first goal: legibility) and stick with it, but know when to break the rules (Waldo Emerson: "A foolish consistency is the hobgoblin of little minds"). Novice programmers tend to overlook the importance of code looks, but as SICP's authors remarked, programs must be written for people to read, and only incidentally for machines to execute. Be careful with the code you write. IndentationUse 2 spaces, no tabs, and never ever mix spaces and tabs. Keep your code under 80-char width whenever possible. Whitespaces
array = [1, 2, 3]
hash = {:a => 1, :b => 2}
def fun(arg1, arg2, arg3); endBut use spaces to let the blocks breathe: [1, 2, 3].map { |x| 2*x }Note that some programmers do put spaces after "[" [ 1, 2, 3 ] and "{" { :a => 1 }. You'll see this style throughout Rails code, for example. I don't recommend it, values seem to "float" due to that space. Naming
Blank linesMy personal rules are:
Example: module Animals
class Dog
attr_accessor :name
attr_accessor :color
def initialize(name, color)
self.name = name
self.color = color
end
def bark
"bark, bark"
end
end
class Cat
....
end
endBlocksSingle line blocks are written with brackets: obj.method { |foo| ... }Multi-line blocks are written with do/end: obj.method do |foo| ... ... end Reginald Braithwaite proposed to use brackets for functional blocks and do/end for blocks with side-effects. While I find the idea interesting, brackets on multi-line blocks look weird, so I wouldn't recommend this style. Multi-line array/hashesThere are many ways of writing multi-line array or hashes. I recommend this style: array = [ 1, 2, 3, ] Note that we insert a comma even after the last element. This is very handy because we have not to worry whether we are writing the last line or no (and you can reorder without further editing). The same idea applies for hashes or the combination of both: hash = {
:a => 1,
:b => {
:b1 => "11",
:b2 => "12",
},
:c => [
"hello",
"there",
],
}Use of parenthesisParenthesis on method definitions/calls are (usually) optional in Ruby. But my personal advice is: just write them. The only valid exception I can thinkk of are DSL-style calls. The best known example are Rails class methods, which look very good indeed: class Post < ActiveRecord::Base belongs_to :blog end Method name qualifiersIf your method returns a boolean, end it with ?: dog.hungry? If your method does does something "dangerous" (an in-place operation, or it may raise an exception on errors, or it's destructive, etc), end it with !: database.destroy! Testing for truth valuesNo, no, no: if !some_object.nil?
..
endYes: if some_object
...
endRuby's objects are always true (in boolean terms) except for nil and false, so the only valid reason to write the verbose object.nil? would be telling nil from false, something hardly needed. Method argumentsNo: def method(arg1, arg2, arg3=1, arg4="hello", arg5="bye") ... end Yes: def method(arg1, arg2, options = {})
options.reverse_update(
:arg3 => 1,
:arg4 => "hello",
:arg5 => "bye",
})
...
endWhy:
Downside:
Calling functions with key argumentsIt's not necessary to use explicit {} to indicate it's a hash: dog.bark(:volumen => 10, :duration => 5) However, if there are lots of options and the line is too long, I recommend this multi-line style: dog.bark({
:volumen => 10,
:duration => 5,
:direction => :ne,
})Unpack enumerables in block argumentsNo: [[1, 2], [3, 5]].map do |array| array[0] - array[1] end # [1, 2] Yes: [[1, 2], [3, 5]].map do |v1, v2| v2 - v1 end # [1, 2] Note that you can expand as nested levels as you need to using parenthesis: [["hello", [2, 3.3]], ["bye", [4, 1.1]]].map do |string, (integer, float)| ... end Use an script both as library or executableif __FILE__ == $0 # this script was not imported but executed, do something interesting here end Catch-all rescue'sWhat do you think of this code? author_name = Book.find(book_id).author.name rescue nil IMO this is plain unacceptable. A silent and indiscriminate rescue catches errors of all kinds (not only the ones you have in mind!), so it becomes a hard-to-debug source of bugs. Just don't use it. Then how to write this code without filling it with conditionals? check the section that covers the "maybe" pattern. Functional RubyRuby is not a functional language. In fact, being a classic OOP language it tacitly promotes changes in the state of objects. But, at the same time, Ruby comes with great functional capabilities. This section was getting pretty long, so I moved it to its own page: RubyFunctionalProgramming. Build your own toolboxExtend with generic functionsSo you have a dog named Scooby and want to know if it's in your list of selected dogs. You would probably write: ["rantanplan", "milu"].include?("scooby") #=> falseNotice how the code reversed the phrasing. Why don't we create an abstraction that reflects how we think about the problem? class Object
def in?(enumerable)
enumerable.include?(self)
end
end"scooby".in?(["rantanplan", "milu"]) #=> false
Use blocks as wrappersNever wrote something similar to this? tries = 0
loop do
begin
process
rescue Exception1, Exception2 => error
tries += 1
retry unless tries > 5
nil
end
endThere is nothing really wrong with this snippet, but look at the relevant code: it's literally buried in a jumble of infrastructure. Let's abstract the high-level construction so: 1) we simplify the code, and 2) we leave a method to be reused on other projects: retry_on_exceptions([Exception1, Exception2], :max_retries => 5) do process end In this second version you see right away what's going on. This example presents a very useful pattern: use blocks as high-level wrappers. The more generic the pattern is, the more reason you should abstract it. This is nothing new, wrappers with callbacks are used in many languages, but the difference with Ruby is how easy is to send anonymous pieces of code as blocks. Some abstractionsManaging nil's. The Maybe patternYou have find a lost dog and you want to know where he lives. You'll write: dog_address = dog.owner.address But hey, there are street dogs with no owners. Then you should be careful and do this instead: dog_address = dog.owner ? dog.owner.address : nil Not nice, but we can live with it. But what happens if the chain continues? let's say we want to know which city the dog comes from: dog_address = dog.owner ? dog.owner.address : nil dog_address_city = dog_address ? dog_address.city : nil We shouldn't be ok with this, each level adds more verbose and repetitive code. There are (at least) three approaches to solve this:
require 'active_support/core_ext/object/try' dog_owner_street = dog.owner.try(:address).try(:street)
Rails' try is not bad, but some programmers don't like seeing that their usual object.method turned into object.wrapper(:method). Ick's maybe (or andand, it's the same idea) takes the proxy approach, you simple insert a maybes the object if it may be nil: dog_owner_street = dog.owner.maybe.address.maybe.street
dog_owner_street = dog.owner.maybe { |owner| owner.address.street }I'd recommend the second one, the maybe proxy approach without blocks: compact yet explicit. Enumerable#map_selectHaskell uses a very cool mathematical construction called list comprehension to build lists. For example, to find out the squares of the first odd natural numbers between 1 and 10: [x^2 | x <- [1..10], odd x] Python smartly borrowed it: [x**2 for x in range(1, 10+1) if x % 2 == 1] But, alas, Ruby has no such construction. However, we can build our poor man's list comprehension, as long as we define which value (usually nil) is to be filtered out from the output. That's how it would look: (1..10).map_select { |x| x**2 if x.odd? }Note that real list-comprehensions are more powerful, as they allow nested iterations. module Enumerable
def map_select(value_for_skip = nil)
self.inject([]) do |acc, item|
value = yield(item)
value == value_for_skip ? acc : acc << value
end
end
endEnumerable#map_detectSimple exercise: find the first element in a list with a square greater than 10 and return this squared value. Options: 1) Simple: detect + operation: n = [1,2,3,4].detect { |x| x**2 > 10 }
result = n ? n**2 : nil # 16Booh, we needed to repeat the operation for the matching value and we also need to control that it's not nil. Ugly. 2) map+first: result = [1,2,3,4].map do |x| square = x**2 square if square > 10 end.compact.first # 16 That'd be conceptually ok in a lazy language, but Ruby maps the whole array, so we'll get very bad performance. 3) Our new abstraction Enumerable#map_detect: result = [1,2,3,4].map_detect do |x| square = x**2 square if square > 10 end # 16 So our map_detect is in fact a lazy version of the second example (map+compact+first). A possible implementation: module Enumerable
def map_detect(value_for_no_matching = nil)
self.each do |member|
if result = yield(member)
return result
end
end
value_for_no_matching
end
endObject#or_elseFirst an intro: operators in other languages are usually methods in Ruby. When you write 2 + 3 this is nothing more than sintantic sugar for 2.+(3). This is cool, because you can override all these methods. But && and || are special in Ruby, you cannot override them because they deal with short-circuited evaluation (and Ruby has no such feature for methods). Ok, now consider this case where an expression in a short-circuit cannot be written in a single expression (expr1 || expr2), you need more: result = expr1 || { expr2a; expr2b; ...; expr2final } # not valid RubyBut this is valid: result = expr1 || begin expr2a; expr2b; ...; expr2final end But what if we want to use blocks for that? we've seen that || is not overridable so we cannot make it accept a block, we must write or own "or" method. Let's call it or_else: result = expr1.or_else do expr2a expr2b expr2final end And that's it, the same short-circuited expressions but now using blocks. The implementation is as simple as it gets: class Object
def or_else(&block)
self || yield
end
endOf course, if you find Object#or_else useful you may probably want to write Object#and_also. Now you can expand these methods with the options yous see fit. For example, it may be nice to decide what is a false value: result = expr1.or_else(:if => :blank?) { alternative }Object#presenceThis code is usually ok: name = database_name || session_name || default_name However, sometimes one of these values will be an empty string (which Ruby considers it to be true) but you don't want empty strings. Activerecord's presence is a good solution, just "nilify" empty values: name = database_name.presence || session_name.presence || default_name.presence Of course you could have written: name = [database_name, session_name, default_name].detect(&:present?) But this would compute all values (|| does not), and they may not be as cheap to calculate as the ones in this example. Object#asLook at this code: result = info[0] * info[1] At first glance it's not clear what it is doing. What about: width, height = info area = width * height Now this is better, we are getting the area of a rectangle. It's really good to name things before using. Well, there is nothing wrong now, but we needed an extra line to unpack the array. How about that?: area = info.as { |width, height| width * height }It's a matter of taste, but I think this is also pretty clear. Another example: y = x**2 pair = [y, y+1] Can be written as: pair = (x**2).as { |y| [y, y+1] }The implementation of Object#as is straighforward: class Object
def as(&block)
yield self
end
endWhat is beautiful code?Beautiful code looks visually good for us poor humans to read, beautiful code abstracts repeated patterns, beautiful code uses the right algorithms, beautiful code is generic and reusable, beautiful code is compact yet understandable. Beautiful code divides instead of spreading complexity, beautiful code uses basic types whenever possible, beautiful code does not sacrifice clarity for efficiency, beautiful code is (almost) self-documented. Ugly code only breeds ugly code. Beautiful code breeds more beautiful code. Conclusion
|
Hi, very useful..it made me to rethink about my coding styles. I was always reluctant to use inject in my code as I found it difficult to read. Your article forced me to have second thoughts about inject.