Writing good code is not an easy task to accomplish and every developer has his weak spots.
For me, developing in a concurrent environment and scheduling between tasks is a serious weak spot.
I know, it may be obvious why these two specific topics might burden the development but I’d like to put it into simple words:
(1) The tiny fact that I cannot read the code line by line and know it really was the order of execution can sometimes make me wish I would have chosen a different profession (not really 🙂 )
Debugging suddenly becomes hell, if not useless, and all these threads pop up to your console from nowhere without having control over them. Not a pleasant experience indeed.
Lucky lucky me, as a Rails developer, I don’t get to deal with concurrency too much (if at all), but that doesn’t mean that (1) is solved for me in Rails – the opposite.
This is exactly the time to present today’s topic – ActiveRecord lazy scope evaluation.
Lazy Evaluation In General
The concept of “lazy evaluation” basically says that you perform the actual calculation only when the value is needed.
Lazy evaluation has many advantages, and what’s beautiful about it that it acts behind the scenes: The developer may not even be aware of it happening ( although as we’ll see further on, the developer must be aware of that ).
I’ll present two of the main advantages of lazy evaluation:
1. It dramatically reduces method calls count in your code:
If + was lazy evaluated, the only assignment statement that would have been executed was
x = 1 + 1 + 1
, instead of the three assignment statements.
2. It prevents the computation of an unused data:
If + was lazy evaluated, none of the assignment statements would have been calculated since the value of x is not used at all.
Now isn’t that just wonderful? You get all these cool tricks for free with no hassle!
It is wonderful, but should be treated carefully. Every Rails developer should always have lazy evaluation somewhere in the back of his mind. The reason for that is that ActiveRecord evaluates scopes lazily, and I’ll show you exactly what that means.
Lazy Evaluation of Scopes in ActiveRecord
I’ll be referring to a simple User model with only one string field, email. Let’s RSpec my point:
I’ll start from the end – this spec fails ( otherwise – why the hell would I be writing this post ).
The spec simply creates 5 users with email fields filled (not blank) and runs the
all_users_have_emails? to verify it returns true.
The naive behavior of the
User.all_users_have_emails? method call would be:
Line 1 should insert
 (an empty array) into
@blanked_email_users since all users have emails.
Line 2 should set the first user email to blank.
Line 3 should return true since
.count returns 0
And still, somehow, this spec fails.
What Went Wrong?
What I was doing wrong is taking myself the privilege to assume that each statement is fully evaluated in the order of line of codes. That’s a wrong assumption.
It is wrong because
User.where(:email => "") in line 1 is lazily evaluated.
Assigning the scope into the instance variable
@blanked_email_users does not act as an evaluation catalyst for that matter.
That means that the query is not executed until we call
@blanked_email_users.count in line 3, and that was done after the data in the database was changed in line 2.
The result is, that even if we call
all_users_have_emails? when all users have emails, we get false as a result since
@blanked_email_users.count (line 3) returns 1 – the first user which his email field was changed in line 2.
Debugging to the Rescue (or not)
The worst thing about it is that debugging this code may make the code behave differently, so the code actually running and the code you are debugging return different outputs.
That may happen if you put a debugger after line 1 and run
eval @blanked_email_user. It will perform the evaluation since debugger runs
inspect on the returned value.
@blanked_email_users will now store an empty array  and the code will return true, but that will happen only when you try to debug the code and force the evaluation of the first line in place.
So next time you debug your Rails code and think that Rails went crazy, think again. You may have been a victim of lazy evaluation.