Il semble évaluation paresseuse d'expressions peut entraîner un programmeur à perdre le contrôle de l'ordre dans lequel leur code est exécuté. J'ai du mal à comprendre pourquoi cela serait acceptable ou souhaité par un programmeur.
Comment ce paradigme peut-il être utilisé pour construire un logiciel prévisible qui fonctionne comme prévu, alors que nous n'avons aucune garantie quand et où une expression sera évaluée?
Beaucoup de réponses vont dans des choses comme des listes infinies et des gains de performances de parties non évaluées du calcul, mais il manque la plus grande motivation pour la paresse: modularité .
L'argument classique est exposé dans l'article très cité "Pourquoi la programmation fonctionnelle est importante" (lien PDF) par John Hughes. L'exemple clé de cet article (Section 5) est de jouer au Tic-Tac-Toe en utilisant l'algorithme de recherche alpha-bêta. Le point clé est (p. 9):
[L'évaluation paresseuse] permet de modulariser un programme comme un générateur qui construit un grand nombre de réponses possibles, et un sélecteur qui choisit la bonne.
Le programme Tic-Tac-Toe peut être écrit comme une fonction qui génère l'arbre de jeu entier à partir d'une position donnée, et une fonction distincte qui le consomme. Au moment de l'exécution, cela ne génère pas intrinsèquement l'ensemble de l'arborescence du jeu, uniquement les sous-parties dont le consommateur a réellement besoin. Nous pouvons changer l'ordre et la combinaison dans lesquels les alternatives sont produites en changeant le consommateur; pas besoin de changer le générateur du tout.
Dans une langue avide, vous ne pouvez pas l'écrire de cette façon car vous passeriez probablement trop de temps et de mémoire à générer l'arbre. Vous vous retrouvez donc soit:
Comment ce paradigme peut-il être utilisé pour construire un logiciel prévisible qui fonctionne comme prévu, alors que nous n'avons aucune garantie quand et où une expression sera évaluée?
Lorsqu'une expression est exempte d'effets secondaires, l'ordre dans lequel les expressions sont évaluées n'affecte pas leur valeur, donc le comportement du programme n'est pas affecté par l'ordre. Le comportement est donc parfaitement prévisible.
Maintenant, les effets secondaires sont une autre affaire. Si des effets secondaires pouvaient survenir dans n'importe quel ordre, le comportement du programme serait en effet imprévisible. Mais ce n'est pas vraiment le cas. Les langages paresseux comme Haskell se font un point d'être référentiellement transparent, c'est-à-dire de s'assurer que l'ordre dans lequel les expressions sont évaluées n'affectera jamais leur résultat. Dans Haskell, cela est obtenu en forçant toutes les opérations avec des effets secondaires visibles par l'utilisateur à se produire à l'intérieur de la monade IO. Cela garantit que tous les effets secondaires se produisent exactement dans l'ordre que vous attendez.
Si vous connaissez les bases de données, un moyen très fréquent de traiter les données est:
select * from foobar
Vous ne contrôlez pas la façon dont le résultat est généré et de quelle manière (index? Analyses complètes de la table?), Ni quand (toutes les données doivent-elles être générées en une seule fois ou de manière incrémentielle lors de la demande?). Tout ce que vous savez, c'est: si il y a plus de données, vous les obtiendrez lorsque vous les demanderez.
L'évaluation paresseuse est assez proche de la même chose. Disons que vous avez une liste infinie définie comme ie. la séquence de Fibonacci - si vous avez besoin de cinq nombres, vous obtenez cinq nombres calculés; si vous avez besoin de 1000, vous obtenez 1000. L'astuce est que le runtime sait quoi fournir où et quand. C'est très, très pratique.
(Les programmeurs Java peuvent émuler ce comportement avec les itérateurs - d'autres langages peuvent avoir quelque chose de similaire)
Pensez à demander à votre base de données une liste des 2 000 premiers utilisateurs dont les noms commencent par "Ab" et ont plus de 20 ans. Ils doivent également être des hommes.
Voici un petit diagramme.
You Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
--------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
--------------------- NOW I'll get them...
WAIT! Make sure they're ---------->---------- Good idea Boss!
over 20!
--------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!
No that is all. :( ---------->---------- FINE! Getting records!
--------------------- Here you go.
Thanks Postgres, you're ---------->---------- ...
my only friend.
Comme vous pouvez le voir par cette terrible et terrible interaction, la "base de données" ne fait rien tant qu'elle n'est pas prête à gérer toutes les conditions. Ce sont des résultats de chargement paresseux à chaque étape et l'application de nouvelles conditions à chaque fois.
Au lieu d'obtenir les 2000 premiers utilisateurs, de les renvoyer, de les filtrer pour "Ab", de les renvoyer, de les filtrer pour plus de 20, de les renvoyer, et de filtrer pour les hommes et enfin de les renvoyer.
Chargement paresseux en bref.
L'évaluation paresseuse des expressions entraînera la perte de contrôle du concepteur d'un morceau de code donné sur la séquence d'exécution de leur code.
Le concepteur ne devrait pas se soucier de l'ordre dans lequel les expressions sont évaluées à condition que le résultat soit le même. En différant l'évaluation, il peut être possible d'éviter d'évaluer complètement certaines expressions, ce qui fait gagner du temps.
Vous pouvez voir la même idée à l'oeuvre à un niveau inférieur: de nombreux microprocesseurs sont capables d'exécuter des instructions dans le désordre, ce qui leur permet d'utiliser plus efficacement leurs différentes unités d'exécution. La clé est qu'ils examinent les dépendances entre les instructions et évitent de réorganiser où cela changerait le résultat.
Il y a plusieurs arguments pour une évaluation paresseuse, je pense, sont convaincants
Modularité Avec l'évaluation paresseuse, vous pouvez diviser le code en plusieurs parties. Par exemple, supposons que vous ayez le problème de "trouver les dix premiers inverses d'éléments dans une liste de liste de sorte que les inverses soient inférieures à 1." Dans quelque chose comme Haskell, vous pouvez écrire
take 10 . filter (<1) . map (1/)
mais c'est juste incorrect dans un langage strict, car si vous lui donnez [2,3,4,5,6,7,8,9,10,11,12,0]
vous diviserez par zéro. Voir la réponse de sacundim pour savoir pourquoi c'est génial dans la pratique
Plus de choses fonctionnent Strictement (jeu de mots), plus de programmes se terminent avec une évaluation non stricte qu'avec une évaluation stricte. Si votre programme se termine par une stratégie d'évaluation "impatiente", il se terminera par une stratégie "paresseuse", mais l'opposé n'est pas vrai. Vous obtenez des choses comme des structures de données infinies (qui sont vraiment un peu cool) comme exemples spécifiques de ce phénomène. Plus de programmes fonctionnent dans des langues paresseuses.
Optimalité L'évaluation des appels par besoin est asymptotiquement optimale par rapport au temps. Bien que les principaux langages paresseux (qui sont essentiellement Haskell et Haskell) ne promettent pas un appel par besoin, vous pouvez plus ou moins vous attendre à un modèle de coût optimal. Les analyseurs de rigueur (et l'évaluation spéculative) réduisent les frais généraux dans la pratique. L'espace est une question plus compliquée.
Force Purity en utilisant une évaluation paresseuse, faire face aux effets secondaires de manière indisciplinée est une douleur totale, car comme vous le dites, le programmeur perd le contrôle. C'est une bonne chose. La transparence référentielle facilite la programmation, la réfraction et le raisonnement sur les programmes. Les langues strictes cèdent inévitablement à la pression d'avoir des morceaux impurs - quelque chose que Haskell et Clean ont admirablement résisté. Cela ne veut pas dire que les effets secondaires sont toujours mauvais, mais les contrôler est si utile que cette seule raison suffit pour utiliser des langages paresseux.
Supposons que vous proposiez de nombreux calculs coûteux, mais ne savez pas lesquels seront réellement nécessaires, ni dans quel ordre. Vous pouvez ajouter un protocole mère-mai-i compliqué pour forcer le consommateur à déterminer ce qui est disponible et à déclencher des calculs qui ne sont pas encore effectués. Ou vous pouvez simplement fournir une interface qui agit comme si tous les calculs étaient effectués.
Supposons également que vous ayez un résultat infini. L'ensemble de tous les nombres premiers par exemple. Il est évident que vous ne pouvez pas calculer l'ensemble à l'avance, donc toute opération dans le domaine des nombres premiers doit être paresseuse.
avec une évaluation paresseuse, vous ne perdez pas le contrôle de l'exécution du code, c'est toujours absolument déterministe. Il est cependant difficile de s'y habituer.
l'évaluation paresseuse est utile parce que c'est un moyen de réduire le terme lambda qui se terminera dans certains cas, où une évaluation désirée échouera, mais pas l'inverse. Cela comprend 1) lorsque vous devez établir un lien avec le résultat du calcul avant d'exécuter réellement le calcul, par exemple, lorsque vous construisez une structure de graphique cyclique, mais que vous souhaitez le faire dans le style fonctionnel 2) lorsque vous définissez une structure de données infinie, mais que vous utilisez ce flux de structure pour n'utiliser qu'une partie de la structure de données.