Blog / Gagnez en perfs avec la méthode lazy en Ruby

La méthode lazy a été ajoutée à Ruby il y a maintenant bien longtemps. Ses capacités sont souvent démontrées sur des listes infinies et les données arrivant en continue sur des flux réseaux.

Pendant bien longtemps je n’en avais pas particulièrement trouvé l’utilité. Je n’avais pas ce genre de problématiques et, si je les rencontrais, les traitais avec un autre langage que Ruby.

Cependant, je pense que cette petite méthode peut se révéler bien utile dans un certain nombre de cas. Ces derniers n’ont pas besoin d’être complexes pour que lazy apporte un gain de performance, simplement en éliminant du travail inutile.

Un cas d’usage

Sur un side-project, nous avons un outil qui peut être configuré pour proposer plusieurs stratégies d’authentification. Ces dernières se succèdent jusqu’à l’obtention d’un résultat.

Dans une application non dynamique, le code aurait cette forme :

user =
  AuthenticationStrategy1.call(request) ||
  AuthenticationStrategy2.call(request) ||
  AuthenticationStrategy3.call(request)

Ruby étant économe, si AuthenticationStrategy1 retourne un utilisateur, les deux autres stratégies ne seront pas appelées. Cela nous va très bien.

Mais… dès que la liste est définie dynamiquement, les choses commencent à se gâter. Pour illustrer cela, utilisons un tableau de fonctions lambda:

auth_strategies = [
  -> { AuthenticationStrategy1.call(request) },
  -> { AuthenticationStrategy2.call(request) },
  -> { AuthenticationStrategy3.call(request) }
]

Comment devrions nous nous y prendre maintenant ? L’approche naïve est d’appeler map et de trouver ensuite le premier résultat grâce à find :

auth_strategies = [
  -> { AuthenticationStrategy1.call(request) },
  -> { AuthenticationStrategy2.call(request) },
  -> { AuthenticationStrategy3.call(request) }
]

auth_strategies
  .map { |strategy| strategy.call }
  .find { |result| result.present? }
Appel stratégie 1
Appel stratégie 2
Appel stratégie 3
Utilisateur trouvé par stratégie 2

Cela fonctionne mais est relativement peu efficace sans compter que ce pourrait même se révéler dangereux si une stratégie a des effets secondaires (si un connexion était décomptée d’un quota par exemple).

Notre amie lazy

C’est là que notre amie lazy se montre utile. En l’ajoutant dans le mix, la chaine entière est modifiée et seules les stratégies nécessaires vont tourner.

auth_strategies = [
  -> { AuthenticationStrategy1.call(request) },
  -> { AuthenticationStrategy2.call(request) },
  -> { AuthenticationStrategy3.call(request) }
]

auth_strategies
  .lazy
  .map { |strategy| strategy.call }
  .find { |result| result.present? }
Appel stratégie 1
Appel stratégie 2
Utilisateur trouvé par stratégie 2

Conclusion

Bon nombre de petits cas comme celui-ci peuvent être traités efficacement et élégament grâce aux lazy enumerators. C’est toujours une bonne technique à se garder sous le coude.

Dernière note : J’ai utilisé ici present? pour être explicite mais si vous travaillez en dehors de Rails et cherchez simplement la première valeur « truthy », vous pouvez vous contenter de passer la méthode itself comme block de la méthode find:

auth_strategies.lazy.map(&:call).find(&:itself)

La première valeur « truthy » sera retournée avec le moins d’opérations possibles.

18 février 2020
Simon Courtois
linkedin - twitter

Simon est passioné par le développement, la tech, le design et l’amélioration continue. CTO et Co-Fondateur chez PDFMonkey, il supervise le développement et l'évolution de la plateforme en s’assurant que le produit respecte toujours nos valeurs fondamentales.