Sonntag, 1. Juni 2014

Meaningful Ruby blocks: do...end vs. {...}

Ruby blocks are the things that do something and can look like:
[1, 2, 3].inject(10) do |sum, number|
  sum += number
end
=> 16
or can have curly brackets:
[1, 2, 3].inject(10) { |sum, number|
  sum += number
}
=> 16
The only syntactic difference between both is the higher precedence of the {} (curly brackets) compared to do...end blocks. And as a side note therefore this is syntactically wrong:
[1, 2, 3].inject 10 { |sum, number| sum += number }
# SyntaxError: compile error
while this one is correct:
[1, 2, 3].inject 10 do |sum, number| sum += number end
=> 16
Besides that meaningless syntactic difference, using only one over the other misses the chance to write meaningful blocks.
Even using the convention to use {} (curly brackets) for one liners and do...end blocks for multi liners does not add any more sense to the code. Everyone can distinct between a one liner and multi liner without that convention. It is just meaningless noise.
But you can add sense by using {} (curly brackets) for functional blocks and do...end blocks for procedural blocks.
The primary purpose of functional blocks is to return a value:
[1, 42, 1024].map { |number| number.to_s.rjust(4, '0') }
=> ["0001", "0042", "1024"]

Dir.glob('*.txt').inject('') { |text, file_name| 
  file = File.open file_name, 'r'
  text << "#{file_name.chomp('.txt')}:#{file.read}"
}
=> "en:Hello! fr:Salut! es:Hola!"
whereas the primary purpose of procedural blocks is to change the state of the system in some way:
numbers = [1, 42, 1024]
numbers.map! do |number| 
  number.to_s.rjust(4, '0')
end
=> ["0001", "0042", "1024"]

Dir.glob('*.txt').each do |file_name| 
  file = File.open file_name, 'w'
  file.write file_name.chomp('.txt')
end
=> ["en.txt" , "fr.txt", "es.txt"]
Both examples look like doing almost similar things. Especially the first example (Enumerable#map and Enumerable#map!) does the same task inside the block, but differs in the way it effects the "outer world". Enumerable#map! not only returns the result, but also changes the state of the numbers array itself.
The second example (Dir#glob) opens a bunch of files in both versions. But while the first one only reads from each file and returns a string, the second writes to each file. And that is exactly the point.
Some points, why this convention makes totally sense:
  1. No need to change the block style from {} to do...end or vice versa, when the number of lines change
  2. Visual cue about the intent of the code that wouldn’t otherwise be there (quick information if the block has side effects or not)
  3. Method chaining onto a functional block (curly brackets) is quite natural (read Chain your Ruby methods!)
And like anytime: a convention is not a rule. It is just a matter of a good coding style.
A big salute goes to Avdi Grimm and Jim Weirich for bringing up this convention.

Supported by Ruby 2.1.1

Chain your Ruby methods!

Keine Kommentare:

Kommentar veröffentlichen