Je viens d'apprendre le fonctionnement de l'évaluation paresseuse et je me demandais: pourquoi l'évaluation paresseuse n'est-elle pas appliquée à tous les logiciels actuellement produits? Pourquoi toujours utiliser une évaluation enthousiaste?
Je viens d'apprendre le fonctionnement de l'évaluation paresseuse et je me demandais: pourquoi l'évaluation paresseuse n'est-elle pas appliquée à tous les logiciels actuellement produits? Pourquoi toujours utiliser une évaluation enthousiaste?
Réponses:
L'évaluation paresseuse nécessite des frais généraux de tenue de livres - vous devez savoir si elle a déjà été évaluée et de telles choses. L'évaluation désireuse est toujours évaluée, vous n'avez donc pas besoin de le savoir. Cela est particulièrement vrai dans des contextes concurrents.
Deuxièmement, il est trivial de convertir une évaluation enthousiaste en une évaluation paresseuse en l'empaquetant en un objet fonction à appeler plus tard, si vous le souhaitez.
Troisièmement, une évaluation paresseuse implique une perte de contrôle. Que faire si j'évalue paresseusement la lecture d'un fichier à partir d'un disque? Ou prendre le temps? Ce n'est pas acceptable.
Une évaluation désirée peut être plus efficace et plus contrôlable, et est trivialement convertie en évaluation paresseuse. Pourquoi voudriez-vous une évaluation paresseuse?
readFile
est exactement ce dont j'ai besoin. De plus, la conversion d'une évaluation paresseuse à une évaluation avide est tout aussi triviale.
head [1 ..]
vous donne dans un langage pur évalué avec impatience, car dans Haskell, cela donne 1
?
Principalement parce que le code et l'état paresseux peuvent mal se mélanger et provoquer des bogues difficiles à trouver. Si l'état d'un objet dépendant change, la valeur de votre objet paresseux peut être incorrecte lors de l'évaluation. Il est préférable que le programmeur code explicitement l'objet pour qu'il soit paresseux lorsqu'il sait que la situation est appropriée.
D'un côté, Haskell utilise l'évaluation paresseuse pour tout. Ceci est possible car c'est un langage fonctionnel et n'utilise pas d'état (sauf dans quelques circonstances exceptionnelles où ils sont clairement marqués)
set!
dans un interprète Scheme paresseux. > :(
L'évaluation paresseuse n'est pas toujours meilleure.
Les avantages de l'évaluation paresseuse en termes de performances peuvent être excellents, mais il n'est pas difficile d'éviter la plupart des évaluations inutiles dans des environnements enthousiastes - certainement paresseux la rend facile et complète, mais rarement l'évaluation inutile dans le code est un problème majeur.
La bonne chose à propos de l'évaluation paresseuse est qu'elle vous permet d'écrire du code plus clair; obtenir le 10e nombre premier en filtrant une liste de nombres naturels infinis et en prenant le 10e élément de cette liste est l'une des façons les plus concises et claires de procéder: (pseudocode)
let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)
Je pense qu'il serait assez difficile d'exprimer les choses avec autant de concision sans paresse.
Mais la paresse n'est pas la réponse à tout. Pour commencer, la paresse ne peut pas être appliquée de manière transparente en présence de l'état, et je pense que l'état ne peut pas être détecté automatiquement (sauf si vous travaillez par exemple dans Haskell, lorsque l'état est assez explicite). Ainsi, dans la plupart des langues, la paresse doit être effectuée manuellement, ce qui rend les choses moins claires et supprime ainsi l'un des grands avantages de l'évaluation paresseuse.
En outre, la paresse présente des inconvénients en termes de performances, car elle entraîne une surcharge importante de conservation des expressions non évaluées; ils utilisent du stockage et ils sont plus lents à travailler qu'avec des valeurs simples. Il n'est pas rare de découvrir que vous devez coder avec impatience car la version paresseuse est lente et il est parfois difficile de raisonner sur les performances.
Comme cela a tendance à se produire, il n'y a pas de meilleure stratégie absolue. Lazy est génial si vous pouvez écrire un meilleur code en tirant parti de structures de données infinies ou d'autres stratégies qu'il vous permet d'utiliser, mais il peut être plus facile d'optimiser.
Voici une brève comparaison des avantages et des inconvénients d'une évaluation avide et paresseuse:
Évaluation désireuse:
Frais généraux potentiels liés à l'évaluation inutile de choses.
Évaluation rapide et sans entrave.
Évaluation paresseuse:
Aucune évaluation inutile.
Frais généraux de comptabilité à chaque utilisation d'une valeur.
Donc, si vous avez de nombreuses expressions qui ne doivent jamais être évaluées, paresseux est préférable; pourtant, si vous n'avez jamais une expression qui n'a pas besoin d'être évaluée, paresseux est une surcharge pure.
Maintenant, jetons un œil aux logiciels du monde réel: combien de fonctions que vous écrivez ne nécessitent pas l' évaluation de tous leurs arguments? Surtout avec les fonctions courtes modernes qui ne font qu'une chose, le pourcentage de fonctions entrant dans cette catégorie est très faible. Ainsi, une évaluation paresseuse introduirait la plupart du temps les frais de tenue de livres, sans avoir la possibilité d'enregistrer quoi que ce soit.
Par conséquent, une évaluation paresseuse ne paie tout simplement pas en moyenne, une évaluation désireuse est la meilleure solution pour le code moderne.
Comme l'a noté @DeadMG, l'évaluation paresseuse nécessite des frais de tenue de livres. Cela peut être coûteux par rapport à une évaluation avide. Considérez cette déclaration:
i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3
Cela prendra un peu de calcul pour calculer. Si j'utilise une évaluation paresseuse, je dois vérifier si elle a été évaluée à chaque fois que je l'utilise. Si cela se trouve dans une boucle étroite fortement utilisée, les frais généraux augmentent considérablement, mais il n'y a aucun avantage.
Avec une évaluation enthousiaste et un compilateur décent, la formule est calculée au moment de la compilation. La plupart des optimiseurs déplacent l'affectation de toutes les boucles dans lesquelles elle se produit, le cas échéant.
L'évaluation paresseuse est la mieux adaptée au chargement de données qui seront rarement consultées et a une surcharge élevée à récupérer. Il est donc plus approprié de border les cas que la fonctionnalité de base.
En général, il est recommandé d'évaluer les éléments fréquemment consultés le plus tôt possible. L'évaluation paresseuse ne fonctionne pas avec cette pratique. Si vous aurez toujours accès à quelque chose, tout ce que l'évaluation paresseuse fera sera d'ajouter des frais généraux. Le coût / avantage de l'utilisation de l'évaluation paresseuse diminue à mesure que l'élément auquel on accède devient moins susceptible d'être consulté.
Toujours utiliser l'évaluation paresseuse implique également une optimisation précoce. Il s'agit d'une mauvaise pratique qui se traduit souvent par un code beaucoup plus complexe et coûteux qui pourrait autrement être le cas. Malheureusement, une optimisation prématurée se traduit souvent par un code plus lent que le code simple. Jusqu'à ce que vous puissiez mesurer l'effet de l'optimisation, c'est une mauvaise idée d'optimiser votre code.
Éviter une optimisation prématurée n'entre pas en conflit avec les bonnes pratiques de codage. Si les bonnes pratiques n'ont pas été appliquées, les optimisations initiales peuvent consister à appliquer de bonnes pratiques de codage telles que le déplacement des calculs hors des boucles.
Si nous devons potentiellement évaluer complètement une expression pour déterminer sa valeur, une évaluation paresseuse peut être un inconvénient. Disons que nous avons une longue liste de valeurs booléennes et que nous voulons savoir si toutes sont vraies:
[True, True, True, ... False]
Pour ce faire, nous devons examiner tous les éléments de la liste, quoi qu'il arrive, il n'y a donc aucune possibilité de suspendre paresseusement l'évaluation. Nous pouvons utiliser un pli pour déterminer si toutes les valeurs booléennes de la liste sont vraies. Si nous utilisons un repli vers la droite, qui utilise l'évaluation paresseuse, nous n'obtenons aucun des avantages de l'évaluation paresseuse, car nous devons examiner chaque élément de la liste:
foldr (&&) True [True, True, True, ... False]
> 0.27 secs
Un pli à droite sera beaucoup plus lent dans ce cas qu'un pli strict à gauche, qui n'utilise pas d'évaluation paresseuse:
foldl' (&&) True [True, True, True, ... False]
> 0.09 secs
La raison en est qu'un repli strict à gauche utilise la récursivité de la queue, ce qui signifie qu'il accumule la valeur de retour et ne crée pas et ne stocke pas en mémoire une grande chaîne d'opérations. C'est beaucoup plus rapide que le pli paresseux à droite car les deux fonctions doivent de toute façon regarder la liste entière et le pli à droite ne peut pas utiliser la récursivité de la queue. Donc, le fait est que vous devez utiliser ce qui est le mieux pour la tâche à accomplir.