Blog / Get better perfs using the lazy method in Ruby
The lazy
method has been added to Ruby a long time ago. It is usually shown as a way to deal with somewhat infinite computations or continues data streams from the network.
I didn’t really use it for a long time as I was not facing those type of issues or wasn’t using Ruby if I did face them.
Nevertheless, I think this tiny method can become very useful in a bunch of use-cases and they don’t need to be that complex to provide a performance improvement just by removing unnecessary computation.
Our use-case
On a side-project, we have a tool that can be configured to provide various authentication strategies and each one will fallback to the next one.
In a non-dynamic app the code would look like this:
user =
AuthenticationStrategy1.call(request) ||
AuthenticationStrategy2.call(request) ||
AuthenticationStrategy3.call(request)
Ruby evaluation being lazy by design, if AuthenticationStrategy1
was to return a user, the two others would be called at all. That perfect.
But… when you’re using a dynamically defined list of authentication strategies, things start to get a bit more complicated. Let’s illustrate this using an array of lambda functions:
auth_strategies = [
-> { AuthenticationStrategy1.call(request) },
-> { AuthenticationStrategy2.call(request) },
-> { AuthenticationStrategy3.call(request) }
]
Now how should we go about it? The most naive approach would be to call map
and then find the first available result using find
:
auth_strategies = [
-> { AuthenticationStrategy1.call(request) },
-> { AuthenticationStrategy2.call(request) },
-> { AuthenticationStrategy3.call(request) }
]
auth_strategies
.map { |strategy| strategy.call }
.find { |result| result.present? }
Calling strategy 1
Calling strategy 2
Calling strategy 3
Found user with stategy 2
This works but is pretty inefficient and could even be considered dangerous if successful strategies have side-effects (if an authentications quota was defined for instance).
Our lazy friend
That’s were our friend lazy
can become useful. By simply adding it to the mix, the whole chain is transformed and only the necessary strategies will run.
auth_strategies = [
-> { AuthenticationStrategy1.call(request) },
-> { AuthenticationStrategy2.call(request) },
-> { AuthenticationStrategy3.call(request) }
]
auth_strategies
.lazy
.map { |strategy| strategy.call }
.find { |result| result.present? }
Calling strategy 1
Calling strategy 2
Found user with stategy 2
Conclusion
A lot of small situations can be solved efficiently and elegantly just by leveraging lazy enumerators in this way. That’s always a good thing to have it up your sleeve.
Last note: Here I’ve used present?
for explicitness but if you’re working outside of Rails and just need to make sure you get a truthy result, you can leverage the itself
method in the find
call:
auth_strategies.lazy.map(&:call).find(&:itself)
The first truthy result will be returned with the minimum necessary computation.