Yet another reason to stop using attr_reader in the code

Dmytro Vasin
5 min readOct 29, 2020

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 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!

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!

Read more:

--

--