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
decided to replace
order && order.errors.full_messages with
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
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)
parsing processwith the
interpretation 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!
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!
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.
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).
begin — rescue local variable
order 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
transaction the block is already closed and we do not have any local variables!
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
@ — 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!
Variable hoisting in Ruby
A few days ago, I found a strange thing to look at the code of my colleagues. The code that caused the error was…
Scope and local variables in Ruby
A quick introduction to scoping variables and method in Ruby