Pourquoi l'évaluation paresseuse est-elle utile?

119

Je me demande depuis longtemps pourquoi l'évaluation paresseuse est utile. Je n'ai encore personne à m'expliquer d'une manière qui ait du sens; la plupart du temps, cela finit par se résumer à «faites-moi confiance».

Remarque: je ne parle pas de mémorisation.

Joel McCracken
la source

Réponses:

96

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.

mipadi
la source
6
Python a évalué paresseusement des listes infinies via des itérateurs
Mark Cidade
4
Vous pouvez en fait émuler une liste infinie en Python à l'aide de générateurs et d'expressions génératrices (qui fonctionnent de la même manière qu'une compréhension de liste): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery
24
Les générateurs facilitent les listes paresseuses en Python, mais d'autres techniques d'évaluation et structures de données paresseuses sont nettement moins élégantes.
Peter Burns
3
J'ai peur de ne pas être d'accord avec cette réponse. J'avais l'habitude de penser que la paresse était une question d'efficacité, mais après avoir utilisé Haskell beaucoup, puis passer à Scala et comparer l'expérience, je dois dire que la paresse compte souvent mais rarement à cause de l'efficacité. Je pense qu'Edward Kmett touche les vraies raisons.
Owen
3
Je ne suis pas d'accord de la même manière, bien qu'il n'y ait pas de notion explicite d'une liste infinie en C en raison d'une évaluation stricte, vous pouvez facilement jouer le même tour dans n'importe quel autre langage (et en fait, dans la plupart des implémentations réelles de tous les langages paresseux) en utilisant des thunks et en passant autour de la fonction pointeurs pour travailler avec un préfixe fini de la structure infinie produite par des expressions similaires.
Kristopher Micinski
71

Un exemple utile d'évaluation paresseuse est l'utilisation de quickSort:

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

Si nous voulons maintenant trouver le minimum de la liste, nous pouvons définir

minimum ls = head (quickSort ls)

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.

Chris Eidhof
la source
19
Plus généralement, take k $ quicksort listne 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.
éphémère du
@ephemient ne veux-tu pas dire O (nk log k)?
MaiaVictor
1
@Viclib Non, je pensais ce que j'ai dit.
éphémère du
@ephemient alors je pense que je ne comprends pas, malheureusement
MaiaVictor
2
@Viclib Un algorithme de sélection pour trouver les k premiers éléments sur n est O (n + k log k). Lorsque vous implémentez le tri rapide dans un langage paresseux et que vous ne l'évaluez que suffisamment loin pour déterminer les k premiers éléments (arrêt de l'évaluation après), il effectue exactement les mêmes comparaisons qu'un algorithme de sélection non paresseux.
éphémère
70

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.

foo x = x + 3

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:

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

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 letqui 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:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

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:

if' True x y = x
if' False x y = y

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.)

Edward KMETT
la source
5
Très agréable; ce sont les vraies réponses. J'avais l'habitude de penser que c'était une question d'efficacité (retarder les calculs pour plus tard) jusqu'à ce que j'utilise Haskell une quantité significative et que je vois que ce n'est vraiment pas la raison du tout.
Owen
11
De plus, bien qu'il ne soit pas techniquement vrai qu'un langage paresseux doit être pur (R par exemple), il est vrai qu'un langage paresseux impur peut faire des choses très étranges (R par exemple).
Owen
4
Bien sûr que oui. Dans un langage strict, récursif letest une bête dangereuse, dans le schéma R6RS, il laisse #fapparaître des aléas dans votre terme partout où nouer le nœud strictement conduit à un cycle! Aucun jeu de mots, mais les letliaisons strictement plus récursives sont raisonnables dans un langage paresseux. La rigueur exacerbe également le fait qu'il wheren'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 #fproblème. Strict whereénonce votre code avec des préoccupations non locales.
Edward KMETT
2
Pourriez-vous expliquer comment la paresse aide à éviter la restriction de valeur? Je n'ai pas pu comprendre cela.
Tom Ellis
3
@PaulBone De quoi parlez-vous? La paresse a beaucoup à voir avec les structures de contrôle. Si vous définissez votre propre structure de contrôle dans un langage strict, il faudra soit utiliser un tas de lambdas ou similaire, soit ça craindra. Parce que ifFunc(True, x, y)va évaluer à la fois xet yau lieu de juste x.
point
28

Il y a une différence entre une évaluation d'ordre normal et une évaluation paresseuse (comme dans Haskell).

square x = x * x

Évaluation de l'expression suivante ...

square (square (square 2))

... avec une évaluation enthousiaste:

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... avec une évaluation de commande normale:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... avec une évaluation paresseuse:

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

C'est parce que l'évaluation paresseuse regarde l'arbre de syntaxe et effectue des transformations d'arbre ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... 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).

Thomas Danecker
la source
25

É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.

Alexey
la source
L'analogie avec le GC m'a un peu aidé, mais pouvez-vous donner un exemple de "supprime un code standard" s'il vous plaît?
Abdul
1
@Abdul, un exemple familier à tout utilisateur ORM: chargement d'association paresseux. Il charge l'association de DB "juste à temps" et libère en même temps un développeur de la nécessité de spécifier explicitement quand le charger et comment le mettre en cache (c'est le passe-partout, je veux dire). Voici un autre exemple: projectlombok.org/features/GetterLazy.html .
Alexey
25

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 .

Norman Ramsey
la source
Cela les a non seulement forcés à garder le langage pur, mais cela leur a aussi permis de le faire, alors que (avant l'introduction de la IOmonade) la signature de mainserait String -> Stringet que vous pouviez déjà écrire des programmes correctement interactifs.
gauche vers
@leftaroundabout: Qu'est-ce qui empêche un langage strict de forcer tous les effets dans une IOmonade?
Tom Ellis
13

Envisagez un programme tic-tac-toe. Cela a quatre fonctions:

  • Une fonction de génération de mouvements qui prend une carte actuelle et génère une liste de nouvelles cartes, chacune avec un mouvement appliqué.
  • Ensuite, il y a une fonction «arbre de déplacement» qui applique la fonction de génération de déplacement pour dériver toutes les positions de carte possibles qui pourraient découler de celle-ci.
  • Il existe une fonction minimax qui parcourt l'arbre (ou peut-être seulement une partie de celui-ci) pour trouver le meilleur coup suivant.
  • Il existe une fonction d'évaluation du plateau qui détermine si l'un des joueurs a gagné.

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.

Paul Johnson
la source
1
[Dans un langage "désireux" (c'est-à-dire conventionnel), cela ne fonctionnera pas car l'arbre de déplacement ne rentrera pas dans la mémoire] - pour Tic-Tac-Toe, il le sera certainement. Il y a au plus 3 ** 9 = 19683 positions à stocker. Si nous stockons chacun dans 50 octets extravagants, c'est presque un mégaoctet. Ce n'est rien ...
Jonas Kölker
6
Oui, c'est mon point. Les langages désireux peuvent avoir une structure propre pour les jeux triviaux, mais doivent compromettre cette structure pour quelque chose de réel. Les langues paresseuses n'ont pas ce problème.
Paul Johnson
3
Pour être juste, cependant, une évaluation paresseuse peut entraîner ses propres problèmes de mémoire. Il n'est pas rare que les gens se demandent pourquoi haskell fait exploser sa mémoire pour quelque chose qui, dans une évaluation impatiente, aurait une consommation de mémoire de O (1)
RHSeeger
@PaulJohnson Si vous évaluez toutes les positions, cela ne fait aucune différence que vous les évaluiez avec empressement ou paresseusement. Le même travail doit être fait. Si vous vous arrêtez au milieu et évaluez seulement la moitié des positions, cela ne fait aucune différence non plus, car dans les deux cas, la moitié du travail doit être effectuée. La seule différence entre les deux évaluations est que l'algorithme est plus joli s'il est écrit paresseusement.
ceving le
12

Voici deux autres points qui, à mon avis, n’ont pas encore été soulevés dans la discussion.

  1. 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.

  2. 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.

Edward Z. Yang
la source
10

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.

Yfeldblum
la source
1
Certaines personnes pourraient dire que c'est vraiment une "exécution paresseuse". La différence est vraiment immatérielle sauf dans des langues raisonnablement pures comme Haskell; mais la différence est que ce n'est pas seulement le calcul retardé, mais aussi les effets secondaires qui y sont associés (comme l'ouverture et la lecture de fichiers).
Owen
8

Considère ceci:

if (conditionOne && conditionTwo) {
  doSomething();
}

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 ...

Romain Linsolas
la source
Je pensais que c'était un court-circuit, pas une évaluation paresseuse.
Thomas Owens
2
C'est une évaluation paresseuse car conditionTwo n'est calculée que si elle est vraiment nécessaire (c'est-à-dire si conditionOne est vraie).
Romain Linsolas
7
Je suppose que le court-circuit est un cas dégénéré d'évaluation paresseuse, mais ce n'est certainement pas une façon courante d'y penser.
rmeador
19
Le court-circuit est en fait un cas particulier d'évaluation paresseuse. L'évaluation paresseuse englobe bien plus que le simple court-circuit, évidemment. Ou qu'est-ce que le court-circuit a au-delà de l'évaluation paresseuse?
yfeldblum
2
@Juliet: Vous avez une définition forte de «paresseux». Votre exemple de fonction prenant deux paramètres n'est pas le même qu'une instruction if de court-circuit. Un court-circuit si l'instruction évite les calculs inutiles. Je pense qu'une meilleure comparaison avec votre exemple serait l'opérateur de Visual Basic "andalso" qui oblige les deux conditions à être évaluées
8
  1. 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.).

  2. 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 forboucles; Haskell a une forfonction. Java gère les exceptions; Haskell a différents types de monades d'exceptions. C # a goto; Haskell a la monade de continuation ...)

  3. 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.

  4. 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.

MathématiqueOrchidée
la source
6

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 #):

type 'a stack =
    | EmptyStack
    | StackNode of 'a * 'a stack

let rec append x y =
    match x with
    | EmptyStack -> y
    | StackNode(hd, tl) -> StackNode(hd, append tl y)

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:

type 'a lazyStack =
    | StackNode of Lazy<'a * 'a lazyStack>
    | EmptyStack

let rec append x y =
    match x with
    | StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
    | Empty -> y

lazyfonctionne 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.

Juliette
la source
5

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

Génération non paresseuse: 0.023373
Génération paresseuse: 0,000009
Sortie non paresseuse: 0,000921
Sortie paresseuse: 0,024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
Vinko Vrsalovic
la source
5

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 learnPatfonction dans le AI.Instinct.Train.Deltamodule). Traditionnellement, les deux nécessitent des algorithmes itératifs beaucoup plus compliqués.

ertes
la source
4

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:

fix f = f (fix f)

cela s'étend à

f (f (f ....

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:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

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 stricte f, 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:

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

Vous obtenez bien sûr un débordement de pile. Si vous voulez que cela fonctionne, vous devez faire fappel à l' argument par besoin:

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)
Owen
la source
3

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.

Noah Lavine
la source
1
Mettre en œuvre une évaluation paresseuse dans des langages stricts est souvent un Tarpit de Turing.
itsbruce
2

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:

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

ou, la solution la plus élégante:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Une fois que vous commencez à l'utiliser, vous verrez des opportunités de l'utiliser de plus en plus souvent.

Marc Bernier
la source
2

Sans évaluation paresseuse, vous ne serez pas autorisé à écrire quelque chose comme ceci:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }
pelures
la source
Eh bien, imo, c'est une mauvaise idée de faire ça. Bien que ce code puisse être correct (en fonction de ce que vous essayez de réaliser), il est difficile à lire, ce qui est toujours une mauvaise chose.
Brann
12
Je ne pense pas. C'est une construction standard en C et ses proches.
Paul Johnson
Ceci est un exemple d'évaluation de court-circuit, pas d'évaluation paresseuse. Ou est-ce effectivement la même chose?
RufusVS
2

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.

shapr
la source
2

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 .

Will Ness
la source
1

Si par "évaluation paresseuse" vous entendez comme dans les booléens combinés, comme dans

   if (ConditionA && ConditionB) ... 

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:

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

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 ...

Charles Bretana
la source
0

Extrait des fonctions d'ordre supérieur

Trouvons le plus grand nombre inférieur à 100 000 qui est divisible par 3829. Pour ce faire, nous allons simplement filtrer un ensemble de possibilités dans lesquelles nous connaissons la solution.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

Nous faisons d'abord une liste de tous les nombres inférieurs à 100 000, par ordre décroissant. Ensuite, nous le filtrons par notre prédicat et comme les nombres sont triés de manière décroissante, le plus grand nombre qui satisfait notre prédicat est le premier élément de la liste filtrée. Nous n'avons même pas eu besoin d'utiliser une liste finie pour notre set de départ. C'est de nouveau la paresse en action. Comme nous finissons par n'utiliser que la tête de la liste filtrée, peu importe si la liste filtrée est finie ou infinie. L'évaluation s'arrête lorsque la première solution adéquate est trouvée.

onmyway133
la source