6

Dans son article "Why Functional Programming Matters", John Hughes affirme que "l'évaluation paresseuse est peut-être l'outil le plus puissant pour la modularisation dans le répertoire du programmeur fonctionnel." Pour ce faire, il fournit un exemple comme celui-ci:Comment l'évaluation paresseuse permet-elle une plus grande modularisation?

Supposons que vous ayez deux fonctions, "infiniteLoop" et "terminationCondition". Vous pouvez effectuer les opérations suivantes:

terminationCondition(infiniteLoop input) 

L'évaluation paresseuse, selon les termes de Hughes «permet de séparer les conditions de terminaison des corps de boucle». Cela est certainement vrai, puisque "terminationCondition" utilisant une évaluation paresseuse signifie que cette condition peut être définie en dehors de la boucle - infiniteLoop cessera d'être exécuté lorsque terminationCondition arrête de demander des données.

Mais les fonctions d'ordre supérieur ne pouvaient-elles pas atteindre la même chose que ci-dessous? Comment une évaluation paresseuse fournit-elle une modularisation qui n'est pas fournie par des fonctions d'ordre supérieur?

+0

Il peut être plus constructif de penser aux fonctions de 'Data.List' au lieu du pseudocode ci-dessus. par exemple. qu'est-ce que 'takeWhile foo. drop 1. filtre (> 0). irez-vous ressembler au monde que vous proposez? Avons-nous toujours une bibliothèque 'Data.List' utile? Malheureusement, je pense que cette question est trop large. – jberryman

+2

Pourquoi un 'infiniteLoop' prendrait-il un' terminationCondition' comme argument? Ce serait fini si c'était le cas. – Bergi

+0

Comment réécrire 'terminationCondition (infiniteLoop (loop input))'? – Bergi

Répondre

11

Oui, vous pouvez utiliser une vérification de terminaison transmise, mais pour que cela fonctionne, l'auteur de infiniteLoop aurait dû prévoir la possibilité de terminer la boucle avec ce genre de condition, et câbler un appel à la condition de terminaison dans leur fonction.

Et même si la condition spécifique peut être transmise en tant que fonction, la "forme" de celui-ci est prédéterminée par l'auteur de infiniteLoop. Que se passe-t-il s'ils me donnent une condition de terminaison appelée "slot" sur chaque élément, mais que j'ai besoin d'accéder aux derniers éléments pour vérifier une sorte de condition de convergence? Peut-être que pour un simple générateur de séquence, vous pourriez trouver le type de condition de terminaison «le plus général possible», mais il n'est pas évident de savoir comment le faire et rester efficace et facile à utiliser. Est-ce que je passe à plusieurs reprises la totalité de la séquence jusque dans la condition de terminaison, au cas où c'est ce qu'elle vérifie? Dois-je forcer mes appelants à emballer leurs conditions de résiliation simples dans un paquet plus compliqué afin qu'ils correspondent au type de condition le plus général?

Les appelants doivent certainement savoir exactement comment la condition de terminaison est appelée afin de fournir une condition correcte. Cela pourrait être un peu dépendant de cette implémentation spécifique. S'ils passent à une implémentation différente de infiniteLoop écrite par un autre tiers, quelle est la probabilité que le même concept pour la condition de terminaison soit utilisé? Avec un infiniteLoop paresseux, je peux laisser tomber dans n'importe quelle exécution qui est censée produire la même séquence.

Et si infiniteLoopn'est pas un simple générateur de séquence, mais génère réellement une structure de données infinie plus complexe, comme un arbre? Si toutes les branches de l'arbre sont générées indépendamment de manière récursive (pensez à un arbre de mouvement pour un jeu comme les échecs), il serait logique de couper différentes branches à différentes profondeurs, en fonction de toutes sortes de conditions.

Si l'auteur original ne s'est pas préparé (soit spécifiquement pour mon cas d'utilisation ou pour une classe de cas d'utilisation suffisamment générale), je n'ai pas de chance. L'auteur de la paresseuse infiniteLoop peut simplement l'écrire de façon naturelle, et laisser chaque appelant individuellement explorer paresseusement ce qu'il veut; ni l'un ni l'autre ne doit en savoir beaucoup sur l'autre. En outre, que se passe-t-il si la décision d'arrêter paresseusement d'explorer la sortie infinie est réellement entrelacée avec (et dépend du) calcul effectué par l'appelant avec cette sortie?Pensez à l'arbre de déménagement d'échecs à nouveau; jusqu'où je veux explorer une branche de l'arbre pourrait facilement dépendre de mon évaluation de la meilleure option que j'ai trouvée dans d'autres branches de l'arbre. Donc, soit je fais ma traversée et le calcul deux fois (une fois dans la condition de terminaison pour retourner un drapeau indiquant infinteLoop d'arrêter, puis encore une fois avec la sortie finie afin que je puisse réellement avoir mon résultat), ou l'auteur de infiniteLoop devait se préparer pour pas seulement une condition de terminaison, mais une fonction compliquée qui retourne aussi la sortie (pour que je puisse pousser tout mon calcul dans la "condition de terminaison"). Pris à l'extrême, j'ai pu explorer la sortie et calculer quelques résultats, les afficher à un utilisateur et obtenir des entrées, puis continuer à explorer la structure de données (sans rappeler infiniteLoop basé sur l'entrée de l'utilisateur). L'auteur original de la paresseuse infiniteLoop n'a pas besoin de savoir que je penserais à faire une telle chose, et cela fonctionnera toujours. Si la pureté est appliquée par le système de type, cela ne serait pas possible avec l'approche de condition de terminaison transmise à moins que le infiniteLoop entier ait été autorisé à avoir des effets secondaires si la condition de terminaison le nécessite (par exemple en donnant à l'ensemble un caractère monadique). interface).

En bref, pour permettre la même souplesse que vous obtiendriez avec évaluation paresseuse en utilisant une infiniteLoop stricte qui prend des fonctions d'ordre supérieur pour le contrôler peut être une grande quantité de complexité supplémentaire à la fois l'auteur de infiniteLoop et son appelant (sauf si une variété de wrappers plus simples sont exposés, et l'un d'entre eux correspond au cas d'utilisation de l'appelant). L'évaluation paresseuse peut permettre aux producteurs et aux consommateurs d'être presque complètement découplés, tout en donnant au consommateur la possibilité de contrôler la quantité de production générée par le producteur. Tout ce que vous pouvez faire de cette façon peut faire avec des arguments de fonction supplémentaires comme vous le dites, mais cela demande au producteur et au consommateur de se mettre d'accord sur un protocole pour le fonctionnement des fonctions de contrôle; et ce protocole est presque toujours soit spécialisé au cas d'utilisation (liant le consommateur et le producteur) soit si compliqué afin d'être complètement général que le producteur et le consommateur sont liés à ce protocole, qui est peu susceptible d'être recréé ailleurs, et ils sont encore attachés ensemble.