Surtout parce que cela peut être plus efficace - les valeurs n'ont pas besoin d'être calculées si elles ne seront pas utilisées. Par exemple, je peux passer trois valeurs dans une fonction, mais en fonction de la séquence d'expressions conditionnelles, seul un sous-ensemble peut être utilisé. Dans un langage comme C, les trois valeurs seraient de toute façon calculées; mais dans Haskell, seules les valeurs nécessaires sont calculées.
Il permet également des trucs sympas comme des listes infinies. Je ne peux pas avoir une liste infinie dans un langage comme C, mais en Haskell, ce n'est pas un problème. Les listes infinies sont utilisées assez souvent dans certains domaines des mathématiques, il peut donc être utile d'avoir la capacité de les manipuler.
Un exemple utile d'évaluation paresseuse est l'utilisation de
quickSort
:Si nous voulons maintenant trouver le minimum de la liste, nous pouvons définir
Lequel trie d'abord la liste et prend ensuite le premier élément de la liste. Cependant, en raison de l'évaluation paresseuse, seule la tête est calculée. Par exemple, si nous prenons le minimum de la liste,
[2, 1, 3,]
quickSort filtrera d'abord tous les éléments inférieurs à deux. Ensuite, il effectue un tri rapide sur cela (retournant la liste des singleton [1]), ce qui est déjà suffisant. En raison de l'évaluation paresseuse, le reste n'est jamais trié, ce qui économise beaucoup de temps de calcul.C'est bien sûr un exemple très simple, mais la paresse fonctionne de la même manière pour les programmes qui sont très volumineux.
Il y a cependant un inconvénient à tout cela: il devient plus difficile de prédire la vitesse d'exécution et l'utilisation de la mémoire de votre programme. Cela ne veut pas dire que les programmes paresseux sont plus lents ou prennent plus de mémoire, mais c'est bon à savoir.
la source
take k $ quicksort list
ne prend que le temps O (n + k log k), oùn = length list
. Avec un tri de comparaison non paresseux, cela prendrait toujours O (n log n) temps.Je trouve l'évaluation paresseuse utile pour un certain nombre de choses.
Premièrement, toutes les langues paresseuses existantes sont pures, car il est très difficile de raisonner sur les effets secondaires dans une langue paresseuse.
Les langages purs vous permettent de raisonner sur les définitions de fonctions en utilisant le raisonnement équationnel.
Malheureusement, dans un paramètre non paresseux, plus d'instructions échouent que dans un paramètre paresseux, ce qui est donc moins utile dans des langages comme ML. Mais dans un langage paresseux, vous pouvez raisonner en toute sécurité sur l'égalité.
Deuxièmement, beaucoup de choses comme la «restriction de valeur» dans ML ne sont pas nécessaires dans les langages paresseux comme Haskell. Cela conduit à un grand désencombrement de la syntaxe. ML comme les langages doivent utiliser des mots-clés comme var ou fun. Dans Haskell, ces choses se résument à une seule notion.
Troisièmement, la paresse vous permet d'écrire un code très fonctionnel qui peut être compris en morceaux. Dans Haskell, il est courant d'écrire un corps de fonction comme:
Cela vous permet de travailler «de haut en bas» grâce à la compréhension du corps d'une fonction. Les langages de type ML vous obligent à utiliser un
let
qui est évalué strictement. Par conséquent, vous n'osez pas «soulever» la clause let vers le corps principal de la fonction, car si elle est coûteuse (ou a des effets secondaires), vous ne voulez pas qu'elle soit toujours évaluée. Haskell peut «pousser» les détails vers la clause where explicitement car il sait que le contenu de cette clause ne sera évalué que si nécessaire.En pratique, nous avons tendance à utiliser des gardes et à les réduire pour:
Quatrièmement, la paresse offre parfois une expression beaucoup plus élégante de certains algorithmes. Un `` tri rapide '' paresseux dans Haskell est une ligne unique et présente l'avantage que si vous ne regardez que les premiers articles, vous ne payez que des coûts proportionnels au coût de sélection de ces articles. Rien ne vous empêche de le faire strictement, mais vous devrez probablement recoder l'algorithme à chaque fois pour obtenir les mêmes performances asymptotiques.
Cinquièmement, la paresse vous permet de définir de nouvelles structures de contrôle dans la langue. Vous ne pouvez pas écrire une nouvelle construction comme «si… alors… autre…» dans un langage strict. Si vous essayez de définir une fonction comme:
dans un langage strict, les deux branches seraient évaluées indépendamment de la valeur de la condition. Cela empire quand on considère les boucles. Toutes les solutions strictes nécessitent que le langage vous fournisse une sorte de citation ou de construction lambda explicite.
Enfin, dans la même veine, certains des meilleurs mécanismes pour faire face aux effets secondaires dans le système de types, tels que les monades, ne peuvent vraiment être exprimés efficacement que dans un cadre paresseux. Ceci peut être observé en comparant la complexité des workflows de F # aux monades Haskell. (Vous pouvez définir une monade dans un langage strict, mais malheureusement, vous échouerez souvent à une ou deux lois de la monade en raison d'un manque de paresse et les flux de travail en comparaison ramassent une tonne de bagages stricts.)
la source
let
est une bête dangereuse, dans le schéma R6RS, il laisse#f
apparaître des aléas dans votre terme partout où nouer le nœud strictement conduit à un cycle! Aucun jeu de mots, mais leslet
liaisons strictement plus récursives sont raisonnables dans un langage paresseux. La rigueur exacerbe également le fait qu'ilwhere
n'y a aucun moyen d'ordonner les effets relatifs, sauf par SCC, c'est une construction au niveau des instructions, ses effets peuvent se produire dans n'importe quel ordre strictement, et même si vous avez un langage pur, vous vous retrouvez avec le#f
problème. Strictwhere
énonce votre code avec des préoccupations non locales.ifFunc(True, x, y)
va évaluer à la foisx
ety
au lieu de justex
.Il y a une différence entre une évaluation d'ordre normal et une évaluation paresseuse (comme dans Haskell).
Évaluation de l'expression suivante ...
... avec une évaluation enthousiaste:
... avec une évaluation de commande normale:
... avec une évaluation paresseuse:
C'est parce que l'évaluation paresseuse regarde l'arbre de syntaxe et effectue des transformations d'arbre ...
... alors que l'évaluation de l'ordre normal ne fait que des expansions textuelles.
C'est pourquoi, lorsque nous utilisons l'évaluation paresseuse, nous devenons plus puissants (l'évaluation se termine plus souvent que les autres stratégies) alors que la performance équivaut à une évaluation impatiente (au moins en notation O).
la source
Évaluation paresseuse liée au processeur de la même manière que le garbage collection lié à la RAM. GC vous permet de prétendre que vous disposez d'une quantité de mémoire illimitée et ainsi de demander autant d'objets en mémoire que vous en avez besoin. Runtime récupérera automatiquement les objets inutilisables. LE vous permet de prétendre que vous disposez de ressources de calcul illimitées - vous pouvez effectuer autant de calculs que nécessaire. Runtime n'exécutera tout simplement pas de calculs inutiles (pour un cas donné).
Quel est l'avantage pratique de ces modèles "fictifs"? Il libère le développeur (dans une certaine mesure) de la gestion des ressources et supprime du code standard de vos sources. Mais le plus important est que vous puissiez réutiliser efficacement votre solution dans un ensemble plus large de contextes.
Imaginez que vous ayez une liste de nombres S et un nombre N.Vous devez trouver le plus proche du nombre N nombre M de la liste S.Vous pouvez avoir deux contextes: un seul N et une liste L de Ns (ei pour chaque N dans L vous recherchez le M le plus proche dans S). Si vous utilisez l'évaluation paresseuse, vous pouvez trier S et appliquer une recherche binaire pour trouver le M le plus proche de N.Pour un bon tri paresseux, il faudra des étapes O (taille (S)) pour N et O simples (ln (taille (S)) * (taille (S) + taille (L))) étapes pour L. uniformément répartie. Si vous n'avez pas d'évaluation paresseuse pour obtenir l'efficacité optimale, vous devez implémenter un algorithme pour chaque contexte.
la source
Si vous croyez Simon Peyton Jones, l'évaluation paresseuse n'est pas importante en soi, mais seulement en tant que «chemise de cheveux» qui a obligé les concepteurs à garder le langage pur. Je suis sensible à ce point de vue.
Richard Bird, John Hughes et, dans une moindre mesure, Ralf Hinze sont capables de faire des choses incroyables avec une évaluation paresseuse. La lecture de leur travail vous aidera à l'apprécier. Le magnifique solveur de Sudoku de Bird et l'article de Hughes sur Why Functional Programming Matters sont de bons points de départ .
la source
IO
monade) la signature demain
seraitString -> String
et que vous pouviez déjà écrire des programmes correctement interactifs.IO
monade?Envisagez un programme tic-tac-toe. Cela a quatre fonctions:
Cela crée une belle séparation claire des préoccupations. En particulier, la fonction de génération de mouvement et les fonctions d'évaluation de la carte sont les seules à avoir besoin de comprendre les règles du jeu: l'arborescence de mouvement et les fonctions minimax sont entièrement réutilisables.
Essayons maintenant d'implémenter les échecs au lieu de tic-tac-toe. Dans un langage "impatient" (c'est-à-dire conventionnel), cela ne fonctionnera pas car l'arbre de déplacement ne rentrera pas dans la mémoire. Alors maintenant, les fonctions d'évaluation de la carte et de génération de mouvements doivent être mélangées avec l'arborescence des mouvements et la logique minimax car la logique minimax doit être utilisée pour décider quels mouvements générer. Notre belle structure modulaire propre disparaît.
Cependant, dans un langage paresseux, les éléments de l'arborescence de déplacement ne sont générés qu'en réponse aux demandes de la fonction minimax: l'arborescence de déplacement entière n'a pas besoin d'être générée avant de laisser minimax lâche sur l'élément supérieur. Ainsi, notre structure modulaire propre fonctionne toujours dans un vrai jeu.
la source
Voici deux autres points qui, à mon avis, n’ont pas encore été soulevés dans la discussion.
La paresse est un mécanisme de synchronisation dans un environnement concurrent. C'est un moyen léger et facile de créer une référence à un calcul et de partager ses résultats entre de nombreux threads. Si plusieurs threads tentent d'accéder à une valeur non évaluée, un seul d'entre eux l'exécutera et les autres se bloqueront en conséquence, recevant la valeur une fois qu'elle sera disponible.
La paresse est fondamentale pour amortir les structures de données dans un environnement pur. Ceci est décrit en détail par Okasaki dans Purely Functional Data Structures , mais l'idée de base est que l'évaluation paresseuse est une forme contrôlée de mutation essentielle pour nous permettre de mettre en œuvre efficacement certains types de structures de données. Alors que nous parlons souvent de paresse nous obligeant à porter la chemise de cheveux pureté, l'inverse s'applique également: il s'agit d'une paire de caractéristiques de langage synergiques.
la source
Lorsque vous allumez votre ordinateur et que Windows s'abstient d'ouvrir chaque répertoire de votre disque dur dans l'Explorateur Windows et s'abstient de lancer chaque programme installé sur votre ordinateur, jusqu'à ce que vous indiquiez qu'un certain répertoire est nécessaire ou qu'un certain programme est nécessaire, que est une évaluation «paresseuse».
L'évaluation «paresseuse» consiste à effectuer des opérations quand et quand elles sont nécessaires. C'est utile lorsqu'il s'agit d'une fonctionnalité d'un langage de programmation ou d'une bibliothèque, car il est généralement plus difficile d'implémenter une évaluation paresseuse par vous-même que de simplement tout pré-calculer à l'avance.
la source
Considère ceci:
La méthode doSomething () ne sera exécutée que si conditionOne est vraie et conditionTwo est vraie. Dans le cas où conditionOne vaut false, pourquoi avez-vous besoin de calculer le résultat de conditionTwo? L'évaluation de conditionTwo sera une perte de temps dans ce cas, surtout si votre condition est le résultat d'un processus de méthode.
C'est un exemple de l'intérêt de l'évaluation paresseuse ...
la source
Cela peut augmenter l'efficacité. C'est celui qui semble évident, mais ce n'est pas vraiment le plus important. (Notez également que la paresse peut tuer l' efficacité aussi - ce fait est pas immédiatement évidente Cependant, en stockant des tas de résultats temporaires plutôt que de calculer les immédiatement, vous pouvez utiliser une énorme quantité de RAM.).
Il vous permet de définir des constructions de contrôle de flux dans un code de niveau utilisateur normal, plutôt que d'être codé en dur dans le langage. (Par exemple, Java a des
for
boucles; Haskell a unefor
fonction. Java gère les exceptions; Haskell a différents types de monades d'exceptions. C # agoto
; Haskell a la monade de continuation ...)Il vous permet de découpler l'algorithme de génération de données de l'algorithme pour décider de la quantité de données à générer. Vous pouvez écrire une fonction qui génère une liste de résultats théoriquement infinie et une autre fonction qui traite autant de cette liste qu'elle le décide. Plus précisément, vous pouvez avoir cinq fonctions de générateur et cinq fonctions de consommateur, et vous pouvez produire efficacement n'importe quelle combinaison - au lieu de coder manuellement 5 x 5 = 25 fonctions qui combinent les deux actions à la fois. (!) Nous savons tous que le découplage est une bonne chose.
Cela vous oblige plus ou moins à concevoir un langage fonctionnel pur . Il est toujours tentant de prendre des raccourcis, mais dans un langage paresseux, la moindre impureté rend votre code extrêmement imprévisible, ce qui milite fortement contre les raccourcis.
la source
Un énorme avantage de la paresse est la possibilité d'écrire des structures de données immuables avec des limites amorties raisonnables. Un exemple simple est une pile immuable (en utilisant F #):
Le code est raisonnable, mais l'ajout de deux piles x et y prend O (longueur de x) temps dans les cas les meilleurs, les pires et les moyens. L'ajout de deux piles est une opération monolithique, elle touche tous les nœuds de la pile x.
Nous pouvons réécrire la structure de données sous la forme d'une pile paresseuse:
lazy
fonctionne en suspendant l'évaluation du code dans son constructeur. Une fois évaluée à l'aide de.Force()
, la valeur de retour est mise en cache et réutilisée à chaque fois.Force()
.Avec la version paresseuse, les ajouts sont une opération O (1): il renvoie 1 nœud et suspend la reconstruction proprement dite de la liste. Lorsque vous obtenez la tête de cette liste, il évaluera le contenu du nœud, le forçant à retourner la tête et créer une suspension avec les éléments restants, donc prendre la tête de la liste est une opération O (1).
Ainsi, notre liste paresseuse est dans un état constant de reconstruction, vous ne payez pas le coût de la reconstruction de cette liste tant que vous n'avez pas parcouru tous ses éléments. En utilisant la paresse, cette liste prend en charge O (1) consing et ajout. Fait intéressant, puisque nous n'évaluons pas les nœuds avant leur accès, il est tout à fait possible de construire une liste avec des éléments potentiellement infinis.
La structure de données ci-dessus n'exige pas que les nœuds soient recalculés à chaque parcours, ils sont donc nettement différents des IEnumerables vanille dans .NET.
la source
Cet extrait montre la différence entre l'évaluation paresseuse et non paresseuse. Bien sûr, cette fonction fibonacci pourrait elle-même être optimisée et utiliser une évaluation paresseuse au lieu de la récursivité, mais cela gâcherait l'exemple.
Supposons que nous POUVONS avoir à utiliser les 20 premiers numéros pour quelque chose, avec pas paresseux évaluation tous les 20 numéros doivent être générés dès le départ, mais avec évaluation paresseuse ils seront générés au besoin seulement. Ainsi, vous ne paierez que le prix de calcul en cas de besoin.
Exemple de sortie
la source
L'évaluation paresseuse est plus utile avec les structures de données. Vous pouvez définir un tableau ou un vecteur de manière inductive en spécifiant uniquement certains points de la structure et en exprimant tous les autres en termes de l'ensemble du tableau. Cela vous permet de générer des structures de données de manière très concise et avec des performances d'exécution élevées.
Pour voir cela en action, vous pouvez jeter un œil à ma bibliothèque de réseaux neuronaux appelée instinct . Il utilise fortement l'évaluation paresseuse pour l'élégance et la haute performance. Par exemple, je me débarrasse totalement du calcul d'activation traditionnellement impératif. Une simple expression paresseuse fait tout pour moi.
Ceci est utilisé par exemple dans la fonction d'activation et aussi dans l'algorithme d'apprentissage de rétropropagation (je ne peux publier que deux liens, vous devrez donc rechercher vous-même la
learnPat
fonction dans leAI.Instinct.Train.Delta
module). Traditionnellement, les deux nécessitent des algorithmes itératifs beaucoup plus compliqués.la source
D'autres personnes ont déjà donné toutes les grandes raisons, mais je pense qu'un exercice utile pour aider à comprendre pourquoi la paresse est importante est d'essayer d'écrire une fonction à virgule fixe dans un langage strict.
Dans Haskell, une fonction en virgule fixe est super simple:
cela s'étend à
mais parce que Haskell est paresseux, cette chaîne infinie de calcul n'est pas un problème; l'évaluation se fait "de l'extérieur vers l'intérieur", et tout fonctionne à merveille:
Surtout, il n'est pas important d'
fix
être paresseux, mais d'f
être paresseux. Une fois que vous avez déjà reçu une strictef
, vous pouvez soit jeter vos mains en l'air et abandonner, soit eta l'étendre et encombrer les choses. (Cela ressemble beaucoup à ce que Noah disait à propos de la bibliothèque stricte / paresseuse, pas du langage).Imaginez maintenant écrire la même fonction en Scala strict:
Vous obtenez bien sûr un débordement de pile. Si vous voulez que cela fonctionne, vous devez faire
f
appel à l' argument par besoin:la source
Je ne sais pas comment vous pensez actuellement, mais je trouve utile de penser à l'évaluation paresseuse comme un problème de bibliothèque plutôt que comme une fonctionnalité de langage.
Je veux dire que dans des langages stricts, je peux implémenter une évaluation paresseuse en construisant quelques structures de données, et dans des langages paresseux (au moins Haskell), je peux demander la rigueur quand je le veux. Par conséquent, le choix de la langue ne rend pas vraiment vos programmes paresseux ou non paresseux, mais affecte simplement ce que vous obtenez par défaut.
Une fois que vous y pensez comme ça, pensez à tous les endroits où vous écrivez une structure de données que vous pourrez utiliser plus tard pour générer des données (sans trop les regarder avant), et vous verrez beaucoup d'utilisations pour paresseux évaluation.
la source
L'exploitation la plus utile de l'évaluation paresseuse que j'ai utilisée était une fonction qui appelait une série de sous-fonctions dans un ordre particulier. Si l'une de ces sous-fonctions échouait (retournait false), la fonction appelante devait immédiatement revenir. Donc j'aurais pu le faire de cette façon:
ou, la solution la plus élégante:
Une fois que vous commencez à l'utiliser, vous verrez des opportunités de l'utiliser de plus en plus souvent.
la source
Sans évaluation paresseuse, vous ne serez pas autorisé à écrire quelque chose comme ceci:
la source
Entre autres choses, les langages paresseux permettent des structures de données infinies multidimensionnelles.
Alors que les schémas, python, etc. autorisent des structures de données infinies unidimensionnelles avec des flux, vous ne pouvez parcourir qu'une seule dimension.
La paresse est utile pour le même problème marginal , mais il convient de noter la connexion coroutines mentionnée dans ce lien.
la source
L'évaluation paresseuse est le raisonnement équationnel du pauvre (auquel on pourrait s'attendre, idéalement, pour déduire les propriétés du code des propriétés des types et des opérations impliquées).
Exemple où cela fonctionne très bien:
sum . take 10 $ [1..10000000000]
. Ce qui ne nous dérange pas d'être réduit à une somme de 10 nombres, au lieu d'un seul calcul numérique direct et simple. Sans l'évaluation paresseuse, bien sûr, cela créerait une liste gigantesque en mémoire juste pour utiliser ses 10 premiers éléments. Ce serait certainement très lent et pourrait provoquer une erreur de mémoire insuffisante.Exemple où ce n'est pas aussi grande que nous aimerions:
sum . take 1000000 . drop 500 $ cycle [1..20]
. Ce qui fera la somme des 1 000 000 de nombres, même s'ils sont dans une boucle plutôt que dans une liste; il faut néanmoins le réduire à un seul calcul numérique direct, avec peu de conditions et peu de formules. Ce qui serait beaucoup mieux que de résumer les 1 000 000 chiffres. Même si dans une boucle, et non dans une liste (c'est-à-dire après l'optimisation de la déforestation).Une autre chose est, il permet de coder dans le style modulo cons de récursivité de queue , et cela fonctionne juste .
cf. réponse connexe .
la source
Si par "évaluation paresseuse" vous entendez comme dans les booléens combinés, comme dans
alors la réponse est simplement que moins le programme consomme de cycles CPU, plus il s'exécutera rapidement ... et si un morceau d'instructions de traitement n'aura aucun impact sur le résultat du programme, alors il est inutile, (et donc un gaspillage de temps) pour les exécuter quand même ...
si otoh, vous voulez dire ce que j'ai connu comme "initialiseurs paresseux", comme dans:
Eh bien, cette technique permet au code client utilisant la classe d'éviter d'avoir à appeler la base de données pour l'enregistrement de données du superviseur, sauf lorsque le client utilisant l'objet Employé a besoin d'accéder aux données du superviseur ... cela accélère le processus d'instanciation d'un employé, et pourtant, lorsque vous avez besoin du superviseur, le premier appel à la propriété Supervisor déclenchera l'appel de la base de données et les données seront récupérées et disponibles ...
la source
Extrait des fonctions d'ordre supérieur
la source