Il semble qu'une évaluation paresseuse des expressions puisse faire perdre au programmeur le contrôle de l'ordre dans lequel son 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?
functional-programming
haskell
akashchandrakar
la source
la source
head . sort
estO(n)
due à la paresse (pasO(n log n)
). Voir Évaluation paresseuse et complexité temporelle .Réponses:
Beaucoup de réponses portent sur des choses comme des listes infinies et des gains de performances de parties non évaluées du calcul, mais il manque la motivation plus grande pour la paresse: la modularité .
L'argument classique est exposé dans le document 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):
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:
la source
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.
la source
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: s'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)
la source
Collection2.filter()
(ainsi que les autres méthodes de cette classe) implémente à peu près l'évaluation paresseuse: le résultat "ressemble" à un ordinaireCollection
, mais l'ordre d'exécution peut être non intuitif (ou du moins non évident). En outre, il existeyield
en Python (et une fonctionnalité similaire en C #, dont je ne me souviens pas du nom) qui est encore plus proche de la prise en charge de l'évaluation paresseuse qu'un Iterator normal.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.
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 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 les filtrer pour les hommes et enfin de les renvoyer.
Chargement paresseux en bref.
la source
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.
la source
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 sorte que les inverses soient inférieures à 1." Dans quelque chose comme Haskell, vous pouvez écrire
mais c'est juste incorrect dans un langage strict, car si vous le 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 pratiquePlus 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 de l'appel 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.
Forces Purity utilisant une évaluation paresseuse rend la gestion des effets secondaires indisciplinée 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 merveilleusement 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.
la source
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.
la source
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.
la source