Yet another reason to stop using attr_reader
in the code
Recently, walking through my project’s code, I found a line that I want to refactor.
Here is the code that I saw:
Since there were no other changes/assignments to the instance_variable
, I
decided to replace order && order.errors.full_messages
with order.errors.full_messages
I pushed a fixed version and CI started to fail!
Then I rolled back all changes and began to peer at the code.
Here is a stripped-down and cleaned up version of the code that I ran into.
Here is an even simpler version of the same problem:
Or you probably saw a different interpretation of the same action:
If you understand why and how these examples work, then you can stop reading ;)
Hoisting of Local variables?
Let’s take a look at each of these examples — and start with the simplest.
You’ve may expect that ruby will raise an error NameError (undefined local variable or method)
— however, the result is simply nil
.
That happens because the variable was defined in the local scope but has not been assigned (since the code did not run over it).
In simple words, Ruby runs over the code twice. During the first run — it allocates variables and sets them to nil (parsing process). On the second run — it assigns (or not) values to variables (interpretation process)
Don’t confuse
parsing process
with theinterpretation processes
. The parser does not care whether local variable ever gets a value. The job of the parser is just to go over the code, find any local variables, and allocate “space” for those variables. More specifically — set them to nil.On the other hand, — the interpretation process — will follow the logical path of the program and see if/when variables get a value and act on it.
To summarize — the fact that some of the code is not executed does not affect whether the variable is in the local scope or not!
I want to mention — Ruby’s variable hoisting is much different from hoisting in JavaScript. In Ruby, every variable becomes available only after the line where the variable is assigned, regardless if that line was executed or not.
Local variable or Method?
I think everyone who worked with Ruby saw the next error:
We see it whenever the interpreter does not find a local variable or method in the current scope.
Yes! The interpreter first looks for a local variable with that name, and only then — it tries to find a method!
The first puts foo
will give us the output of a method.
Although ruby-parser has already declared the local variable local_variables # => [:foo]
it is still not available, since the ruby-interpreter has not reached the declaration of this variable.
Initial example:
Let's rollback to our initial example. What happens in it?
Let’s walk through it step by step
Every time you define a class, a module, or a method you define a new scope.
Additionally, every time you enter a block, a new scope is introduced.
Any variables you define inside the block cannot be accessed from outside.
The main concern here — is that ActiveRecord::Base.transaction do
creates a new local scope
within which our ruby-parser defines a local variable (we set order = 'local order' if false
in a first rescue).
Note, between begin — rescue
local variableorder
is not yet available — that is why ruby-interpreter trying to find a method with a similar name and call it.
When we do puts order
at the last time, it takes a method-value
because the transaction
the block is already closed and we do not have any local variables!
Conclusion:
As you can see, hoisting in Ruby is not as easy as it might seem at first glance. And subtle mistakes can be easily made.
Moreover, a combination of attr_readers + local_variables + hoisting
could bring more problems than benefits.
With simple exchange attr_reader
to @
— we can avoid all those issues. We can stop thinking about all these local variable declaration rules and scopes!
If you want to use attr_reader
only to call instance_variable
— I would suggest thinking a few more times!