Please stop using ||= all the time
Mat
Posted on April 18, 2022
In ruby code I often see people writing code like
def foo
@foo ||= 'bla bla bla'
end
Instead of
def initialize
@foo = 'bla bla bla'
end
attr_reader :foo
The duck operator ||=
is used to lazily initialize the instance variable, instead of initializing the instance variable in the initialize
method.
This is possible in ruby because assignments are also expressions, and all methods have an implicit return value. So @foo ||= 123
always evaluates to 123
but never assigns to @foo
more than once.
While this is very concise, I found it odd when I first came to ruby after coding in other languages. It feels like a very roundabout way of doing things. If you are initializing instance variables why not use the initializer?
The lazy initialization pattern is one way to avoid running an expensive computation that might not be needed. However, if you know that the initialization is always going to be required for the object to be usable, then there are no performance benefits to structuring it this way.
Structuring code like this makes dependencies a bit harder to notice just from looking at the class. Users of the class might be misled into thinking that the object is fully initialized when it is not, and this can make it harder to initialize objects in a test or in other parts of the codebase.
In practice, I get suspicious even when the initialization is optional, because the optional initialization is still separate from the initializer and so it is less obvious when reading the code. Optional behaviour can also be sign that the class has more than one responsibility.
You can always refactor the class to avoid this. For example, consider using dependency injection to depend on the result of the computation instead of doing the computation internally.
def initialize(foo: nil)
@foo = foo
end
# ...
# methods that use `foo`
# ...
private
attr_reader :foo
Lazy initialization is a special case of temporal coupling: the behaviour of the class is allowed to vary depending on what order you call its methods in. The more complex the behaviour, the harder it is to reason about the code and the easier it is to introduce bugs. In this case, if other methods of your class depend on the instance variable, then they could read nil
if the initializer method isn't called first.
Side note: I see this problem a lot with rails controllers, which do not get initialized on a per-request basis, and force you to set instance variables within your action methods as a way to communicate with the view. This design is unfortunate, but you can still avoid the problem elsewhere in your codebase.
On the other hand, if all the initialization for the class is handled by the initialize
method, it's quite easy to make the behaviour of the class predictable. You can use command-query responsibility segregation (CQRS) to ensure that messages you send to the object either ask for information, or change the objects state, but not both.
Posted on April 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.