The Ruby Style Guide: setting standards
Bozhidar Batsov's created the Ruby Style Guide in 2011 to rectify what he saw as a deficiency. Python has an official style guide; Ruby hasn't.
A style guide within a team or an organisation is an extremely good idea. It makes code more consistent, which makes it easier to read. And it prevents conflict between Developers with different preferences. A baseline style guide is also a good idea. The guide presently has 296 separate entries – that's an awful lot of work if starting from scratch.
The guide's authors are clear about their principles and goals:
In truth the Style Guide goes beyond merely stylistic points. Some guidelines clear up potentially ambiguous syntax (see the entry on and / or). Some favour efficiency (see reverse-each). Others may have different benefits. And some guidelines have extensive explanation while others have none at all.
All this is to the good. However I find that some guidelines favour established practice over objectively better alternatives. The guide meets its objective of promoting consistent code. But it sometimes falls short of promoting better code. In some cases the Ruby Style Guide can be actively harmful.
I going to discuss three of these below (links into the styleguide, not to article chapters):
I talk about the impact of RuboCop, and give some brief conclusions.
Tabs or Spaces?
Tabs or Spaces? Use only spaces for indentation. No hard tabs.
Indentation. Use two spaces per indentation level (aka soft tabs).
The guide doesn't justity these guidelines. I have to assume they refelect existing widespread practice, and I have to object.
Personally I also prefer spaces to tabs for indentation. It's an aesthetic point really. But remember it's not about subjectively better alternatives. Indentation with tabs has an important functional advantage: accessibility.
I like two-space indentation, just like the Style Guide. But my colleague may have a visual impairment or cognitive processing difference. Two spaces may be difficult for them, but tab indentation lets them adjust tab width in their code editor. They can meet their accessibility needs.
The guideline robs them of this ability. It literally disables them if they find two-space indentation inaccessible. It potentially pushes them out of the workplace as Developers.
The guideline should be: Tabs to indent, spaces to align.
Nested Ternary Operators
Nested Ternary Operators. Use one expression per branch in a ternary operator. This also means that ternary operators must not be nested. Prefer if/else constructs in these cases.
Developers in many languages seem to feel that ternary operators are hard to read. I guess they can be if used poorly. So can pretty much any of our tools. Nested ternaries used carefully can be far more readable than alternatives.
Let's consider nested ternary operators:
outcome = condition_1 ? branch_1 : (condition_2 ? branch_2 : branch_3)
Without the parentheses it looks more like a chain:
outcome = condition_1 ? branch_1 : condition_2 ? branch_2 : branch_3
I agree that's not very readable. High cognitive load, good chance of making errors. But what if we reformat it?
outcome =
condition_1 ? branch_1 :
condition_2 ? branch_2 :
branch_3
Now that's very readable.
Let's look at a real-world example I created recently. (I originally wrote this in Javascript – you may recognise it from my recent articles.)
major, minor, micro =
type == :major ? [major + 1, 0, 0] :
type == :minor ? [major, minor + 1, 0] :
[major, minor, micro + 1]
With little visual overhead this is extremely readable. if / else and case / when versions are much more cluttered. And their evaluations are visually more separated from their conditions.
The logic here is also extremely clear. Each branch has the same structure, which communicates underlying semantics very directly. But I only achieved this by refactoring towards nested ternary operators. My original if statement was more obvious to write. But it was also longer and reflected the semantics less clearly:
if type == :major
major += 1
minor = 0
micro = 0
elsif type == :minor
minor += 1
micro = 0
else
micro += 1
end
Surely the job of a style guide is to show users how to use a tool like this helpfully. Instead the Ruby Style Guide gives in to fears of poor formatting. It tells us not to use nested ternaries at all. Not to practice them, not to learn from others' experience, not to improve.
Standard Exceptions
Standard Exceptions. Prefer the use of exceptions from the standard library over introducing new exception classes.
Like the two guidelines above, the Ruby Style Guide provides no explanation for this one. It may be no coincidence that their weaker guidelines come without justification.
There certainly are times when a new exception class is inappropriate:
class MyClass
def my_method(i)
raise MyArgumentError unless i.is_a? Integer
...
end
class MyArgumentError << ArgumentError;
end
`MyArgumentError` adds to my codebase, and the cognitive load to process it. But it doesn't really add anything to `ArgumentError`.
However consider a superficially similar case: analysing a directed acyclic graph:
class DirectedAcyclicGraph
def initialize(vertices, edges)
...
raise(DirectedAcyclicError, 'graph is cyclic') if @graph.cyclic?
...
end
class DirectedAcyclicError << StandardError;
end
In this case DirectedAcyclicError might be an important part of the DirectedAcyclicGraph interface. Particularly if `initialize` may raise other errors. My calling code can handle `DirectedAcyclicError`. I'll let `StandardError` be handled at a higher level.
So the guideline would be better if it were more descriptive:
Prefer the use of exceptions from the standard library over introducing new exception classes. New exception classes should always create new useful semantics.
An explanation like this would move the community towards better practice.
The Ruby Style Guide, consistency and RuboCop
The Ruby Style Guide tells us that we should follow it flexibly (A Note about Consistency):
However, know when to be inconsistent — sometimes style guide recommendations just aren’t applicable. When in doubt, use your best judgment. Look at other examples and decide what looks best. And don’t hesitate to ask!
and (amongst other points)
When applying the guideline would make the code less readable, even for someone who is used to reading code that follows this style guide.
That's a promising approach to enabling us to apply our own best judgement.
However the following year Bozhidar Batsov created Bozhidar Batsov created a linter called RuboCop. This was "as an attempt to make it easy to apply consistently the guidelines from the community Ruby Style Guide" (Rubocop history page).
RuboCop is now the de facto Ruby linter (others are available, eg Rufo or Standard Ruby). It includes rules ('cops') that aren't part of the Ruby Style Guide and omits some that are.
However in the main RuboCop enforces the decisions of the Ruby Style Guide. Why do we do it this way? Because RuboCop says so. Why don't we change the settings? Because that's what's in the guide.
Furthermore, permitting a line of code to break a cop requires one of the following:
The Ruby Style Guide promises that we should allow inconsistency when appropriate. None of these really achieves that.
Concluding thoughts
The Ruby Style Guide is an invaluable stick in the sand as to good Ruby style. However some of its opinions are harmful – we need to treat it as a starting point not a gospel.
RuboCop is an invaluable linting tool. However it makes it difficult to use the Ruby Style Guide flexibly. Together they can overwhelm our own judgement in our own organisation.
You should probably start any new Ruby project with the Style Guide in your back pocket and RuboCop in your Gemfile. But handle with care. Don't be afraid to challenge them, to do what's right in your context.
Set your own standards.
Thanks for reading
This is obviously an opinion piece. I'd really like people to argue with me on it! I'm sure many of you will have other views on parts of this article. You may disagree vehemently. I'd like to read your opinions and see where that takes us.
Thank you.
Image credit
weights by Tanya Hart under CC Sharealike 2.0 license, cropped and scaled
#Ruby #RubyStyleGuide #RuboCop #CodeStandards #Linting