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.

February 18, 2020
Simon Courtois
linkedin - twitter

Simon is passionate about development, tech, design and continuous enhancement. CTO and Co-Founder at PDFMonkey he supervises the development and evolution of the platform making sure the product always respects our core values.