Question

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

Remarque: je ne veux pas dire mémoization.

Était-ce utile?

La solution

Principalement parce que cela peut être plus efficace - les valeurs n'ont pas besoin d'être calculées si elles ne sont pas utilisées. Par exemple, je peux passer trois valeurs dans une fonction, mais selon la séquence d'expressions conditionnelles, seul un sous-ensemble peut être utilisé. Dans un langage comme C, les trois valeurs seraient quand même calculées; mais dans Haskell, seules les valeurs nécessaires sont calculées.

Cela permet également de faire des choses intéressantes 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 étant utilisées assez souvent dans certains domaines des mathématiques, il peut être utile de pouvoir les manipuler.

Autres conseils

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)

Ce qui trie d'abord la liste puis prend le premier élément de la liste. Cependant, à cause d'une é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 dont la taille est inférieure à deux. Ensuite, il effectue un tri rapide (renvoyant la liste des singleton [1]), ce qui est déjà suffisant. En raison d'une évaluation paresseuse, le reste n'est jamais trié, ce qui permet de gagner beaucoup de temps de calcul.

Ceci est bien sûr un exemple très simple, mais la paresse fonctionne de la même manière pour les programmes très volumineux.

Il y a cependant un inconvénient à tout cela: il devient plus difficile de prévoir la vitesse d'exécution et l'utilisation de la mémoire de votre programme. Cela ne signifie pas que les programmes paresseux sont plus lents ou prennent plus de mémoire, mais il est bon de le savoir.

Je trouve l’évaluation paresseuse utile pour un certain nombre de choses.

Tout d'abord, tous les langages paresseux existants sont purs, car il est très difficile de raisonner sur les effets secondaires dans un langage paresseux.

Les langages purs vous permettent de raisonner sur les définitions de fonctions en utilisant un raisonnement équationnel.

foo x = x + 3

Malheureusement, dans un paramètre non paresseux, plus d'instructions échouent que dans un paramètre paresseux. Cette option est donc moins utile dans des langages tels que 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 des langages paresseux comme Haskell. Cela conduit à un grand désencombrement de la syntaxe. ML comme les langues doivent utiliser des mots clés comme var ou fun. En Haskell, ces choses se résument en une notion.

Troisièmement, la paresse vous permet d’écrire un code très fonctionnel qui peut être compris par morceaux. En Haskell, il est courant d’écrire un corps de fonction tel que:

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 si vous comprenez le 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 «lever» la clause let au 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 explicitement "repousser" les détails de la clause where, car il sait que le contenu de cette clause ne sera évalué qu'en fonction des besoins.

Dans la pratique, nous avons tendance à utiliser des protections et à nous effondrer davantage 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 un one-line et présente l'avantage que si vous ne regardez que les premiers éléments, vous ne payez que des coûts proportionnels au coût de la sélection de ces éléments. 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 un nouveau 'if .. then .. else ..' comme construit dans un langage strict. Si vous essayez de définir une fonction telle que:

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

dans un langage strict, les deux branches seraient évaluées quelle que soit la valeur de la condition. Il y a pire quand on considère les boucles. Toutes les solutions strictes nécessitent que le langage vous fournisse une sorte de citation ou une construction lambda explicite.

Enfin, dans la même veine, certains des meilleurs mécanismes pour traiter les effets secondaires dans le système de typage, tels que les monades, ne peuvent vraiment être exprimés efficacement que dans un environnement paresseux. Ceci peut être constaté en comparant la complexité des flux de travail de F # aux monades Haskell. (Vous pouvez définir une monade dans un langage strict, mais malheureusement vous échouerez souvent dans une loi sur la monade en raison du manque de paresse et de workflows comparés pour prendre une tonne de bagages strict.)

Il existe une différence entre l’évaluation des ordres normaux et les évaluations paresseuses (comme dans Haskell).

square x = x * x

Évaluation de l'expression suivante ...

square (square (square 2))

... avec une évaluation avide:

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

... avec évaluation des commandes 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 é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

En effet, une évaluation paresseuse examine l’arbre de syntaxe et effectue des transformations d’arbre ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

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

           ||
           \/

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

... alors que l'évaluation d'ordre normal ne fait que des développements textuels.

C’est pourquoi, lorsque nous utilisons une évaluation paresseuse, nous obtenons plus de puissance (l’évaluation se termine plus souvent que d’autres stratégies) alors que la performance est équivalente à une évaluation avide (au moins en notation O).

Les évaluations paresseuses concernent le processeur de la même manière que la récupération de place liée à la RAM. GC vous permet de prétendre que vous avez une quantité de mémoire illimitée et de demander ainsi autant d'objets en mémoire que nécessaire. L'exécution récupérera automatiquement les objets inutilisables. LE vous permet de prétendre que vous avez des ressources de calcul illimitées - vous pouvez faire 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 "faire semblant"? des modèles? 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 plus grand nombre de contextes.

Imaginez que vous avez une liste de nombres S et un nombre N. Vous devez trouver le nombre le plus proche du nombre N numéro M de la liste S. Vous pouvez avoir deux contextes: un seul N et une liste L de N (ei pour chaque N en L vous cherchez le M le plus proche en S). Si vous utilisez une évaluation paresseuse, vous pouvez trier S et appliquer une recherche binaire pour trouver le plus proche de M à N. Pour effectuer un bon tri paresseux, il faut O (taille (S)) pour N simple et O (ln (taille (S)) * (taille (S) + taille (L))) étapes pour une distribution égale de L. Si vous n'avez pas d'évaluation lazy pour obtenir l'efficacité optimale, vous devez implémenter un algorithme pour chaque contexte.

Si vous en croyez Simon Peyton Jones, une évaluation paresseuse n’est pas importante en soi , mais uniquement en tant que "chemise de cheveux" qui a obligé les concepteurs à garder la langue pure. Je me trouve sympathique à ce point de vue.

Richard Bird, John Hughes et, dans une moindre mesure, Ralf Hinze sont capables de faire des choses étonnantes avec une évaluation paresseuse. La lecture de leur travail vous aidera à l'apprécier. Le magnifique solveur de Sudoku de Bird et le document de Hughes sur Pourquoi la programmation fonctionnelle est importante .

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

  • Une fonction de génération de mouvements qui prend un tableau actuel et génère une liste de nouveaux tableaux avec chacun un mouvement appliqué.
  • Ensuite, il y a un "arbre de déplacement". fonction qui applique la fonction de génération de mouvements pour dériver toutes les positions possibles du conseil pouvant découler de celle-ci.
  • Il existe une fonction minimax qui parcourt l’arbre (ou éventuellement seulement une partie de celui-ci) pour trouver le meilleur coup suivant.
  • Il existe une fonction d'évaluation du tableau qui détermine si l'un des joueurs a gagné.

Cela crée une séparation claire et nette des préoccupations. En particulier, la fonction de génération de mouvements et les fonctions d’évaluation du tableau sont les seules à comprendre les règles du jeu: les fonctions de déplacement et de minimax sont entièrement réutilisables.

Maintenant, essayons d’implémenter les échecs au lieu de tic-tac-toe. Dans un " impatients " (c’est-à-dire conventionnel), cela ne fonctionnera pas car l’arbre de déplacement ne tient pas dans la mémoire. Alors maintenant, les fonctions d’évaluation des cartes et de génération de mouvements doivent être combinées avec l’arbre de déplacements et la logique minimax, car la logique minimax doit être utilisée pour décider des déplacements à générer. Notre belle structure modulaire et propre disparaît.

Cependant, dans un langage paresseux, les éléments de l’arborescence des déplacements ne sont générés que pour répondre aux demandes de la fonction minimax: il n’est pas nécessaire de générer l’arborescence entière avant de laisser lâcher minimax en haut de l’élément supérieur. Notre structure modulaire épurée fonctionne donc toujours dans un vrai jeu.

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

  1. La paresse est un mécanisme de synchronisation dans un environnement concurrent. C'est un moyen simple et léger de créer une référence à un calcul et de partager ses résultats entre plusieurs 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 dès qu'elle sera disponible.

  2. La paresse est essentielle pour amortir les structures de données dans un contexte pur. Ceci est décrit par Okasaki dans Structures de données purement fonctionnelles , mais l’idée de base est que l’évaluation paresseuse est une forme contrôlée de mutation qui nous permet 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 pureté, l’inverse s’applique également: ce sont deux caractéristiques linguistiques synergiques.

Lorsque vous allumez votre ordinateur et que Windows s'abstient d'ouvrir tous les répertoires de votre disque dur dans l'Explorateur Windows et s'abstient de lancer tous les programmes installés sur votre ordinateur, jusqu'à ce que vous indiquiez qu'un répertoire est nécessaire ou qu'un programme est en cours. nécessaire, c’est " paresseux " évaluation.

" Lazy " L’évaluation consiste à effectuer des opérations quand et comme elles sont nécessaires. C'est utile quand il s'agit d'une fonctionnalité d'un langage de programmation ou d'une bibliothèque car il est généralement plus difficile de mettre en œuvre une évaluation paresseuse que de simplement tout pré-calculer à l'avance.

  1. Cela peut augmenter l'efficacité. C’est l’aspect évident, mais ce n’est pas réellement le plus important. (Notez également que la paresse peut également tuer l'efficacité - ce fait n'est pas évident. Cependant, en stockant de nombreux résultats temporaires au lieu de les calculer immédiatement, vous pouvez utiliser une quantité énorme 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 de les coder en dur dans le langage. (Par exemple, Java a des boucles pour ; Haskell a une fonction pour . Java a une gestion des exceptions; Haskell a différents types de monades d'exception. C # a goto ; Haskell a la suite monade ...)

  3. Il vous permet de découpler l'algorithme de génération de données à partir de l'algorithme de décision 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 que nécessaire. Plus précisément, vous pouvez avoir cinq fonctions du générateur et cinq des fonctions de consommation, et vous pouvez efficacement produire toute 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 sauvagement imprévisible, ce qui milite fortement contre la prise de raccourcis.

Considérez ceci:

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

La méthode doQuelque chose () ne sera exécutée que si conditionOne est vraie et conditionTwo est vraie. Dans le cas où conditionOne est fausse, pourquoi devez-vous calculer le résultat de la conditionTwo? Dans ce cas, l’évaluation de conditionTwo sera une perte de temps, surtout si votre condition résulte d’un processus quelconque.

C’est un exemple de l’intérêt de l’évaluation paresseuse ...

Un des grands avantages 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 (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 (durée de x) fois dans les cas les meilleurs, les pires et les cas moyens. L'ajout de deux piles est une opération monolithique, il touche tous les nœuds de la pile x.

Nous pouvons réécrire la structure de données sous forme de 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

lazy suspend 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 .Force () .

Avec la version lazy, les ajouts sont une opération O (1): elle renvoie 1 nœud et suspend la reconstruction réelle de la liste. Lorsque vous obtenez la tête de cette liste, il évalue le contenu du nœud, le forçant à retourner la tête et à créer une suspension avec les éléments restants. Prendre la tête de la liste est donc une opération O (1).

Ainsi, notre liste paresseuse est dans un état constant de reconstruction, vous ne payez pas le coût de cette reconstruction tant que vous n'avez pas parcouru tous ses éléments. En utilisant la paresse, cette liste prend en charge la consignation et l’ajout de O (1). Fait intéressant, comme nous n’évaluons pas les nœuds tant qu’ils n’ont pas été consulté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 de IEnumerables vanille dans .NET.

Une évaluation paresseuse est plus utile avec les structures de données. Vous pouvez définir un tableau ou un vecteur inductif en spécifiant uniquement certains points de la structure et en exprimant tous les autres en termes de tableau entier. 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 consulter ma bibliothèque de réseaux de neurones appelée instinct . . Il fait un usage intensif de l’évaluation paresseuse pour obtenir une élégance et des performances élevées. 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 fonction d'activation et également dans l'algorithme d'apprentissage de la rétropropagation (je ne peux publier que deux liens; vous devrez donc rechercher la fonction learnPat dans AI.Instinct.Train.Delta vous-même). Traditionnellement, les deux nécessitent des algorithmes itératifs beaucoup plus compliqués.

Cet extrait montre la différence entre une é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 d'une récursion, mais cela gâcherait l'exemple.

Supposons que nous pouvons utiliser les 20 premiers nombres pour quelque chose. Avec une évaluation non paresseuse, tous les 20 numéros doivent être générés à l'avance, mais avec une évaluation paresseuse, ils seront générés au besoin. seulement. Ainsi, vous ne paierez que le prix de calcul si nécessaire.

Exemple de sortie

Not lazy generation: 0.023373
Lazy generation: 0.000009
Not lazy output: 0.000921
Lazy output: 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)

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 un point fixe fonctionne dans un langage strict.

En Haskell, une fonction de point fixe est très facile:

fix f = f (fix f)

cela se développe en

f (f (f ....

mais parce que Haskell est paresseux, cette chaîne de calcul infinie n’est pas un problème; l'évaluation est faite "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)

De manière importante, il importe peu que correction soit paresseux, mais que f soit paresseux. Une fois que vous avez déjà reçu un f strict, vous pouvez jeter les mains en l'air et abandonner, ou bien éta le développer et l'encombrer. (Cela ressemble beaucoup à ce que Noah disait de la bibliothèque stricte / paresseuse, pas de la langue).

Maintenant, imaginez é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 avez bien sûr un débordement de pile. Si vous voulez que cela fonctionne, vous devez définir l'argument f call-by-need:

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

Je ne sais pas comment vous pensez les choses, mais je trouve utile de penser que l'évaluation paresseuse est un problème de bibliothèque plutôt qu'une fonctionnalité linguistique.

Je veux dire que dans des langages stricts, je peux mettre en œuvre 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 souhaite. Par conséquent, le choix de la langue ne rend pas vraiment vos programmes paresseux ou non paresseux, mais affecte simplement ceux que vous obtenez par défaut.

Une fois que vous y réfléchissez de la sorte, pensez à tous les endroits où vous écrivez une structure de données que vous pourrez utiliser pour générer des données (sans trop la regarder avant), et vous verrez beaucoup utilise pour l'évaluation paresseuse.

L’exploitation la plus utile de l’évaluation paresseuse que j’ai utilisée est une fonction qui appelle une série de sous-fonctions dans un ordre particulier. Si l'une de ces sous-fonctions échouait (renvoyait la valeur false), la fonction appelante devait immédiatement renvoyer. 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 possibilités de l'utiliser de plus en plus souvent.

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

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

Entre autres choses, les langages paresseux permettent des structures de données infinies multidimensionnelles.

Bien que les schémas, python, etc. permettent des structures de données infinies à une dimension avec des flux, vous ne pouvez les parcourir que le long d'une 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.

L'évaluation paresseuse est le raisonnement équationnel du pauvre (on pourrait s'attendre, idéalement, à déduire des propriétés de code de propriétés de types et d'opérations impliquées).

Exemple où cela fonctionne assez bien: sum. prenez 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 une évaluation paresseuse, 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 génial que nous voudrions: sum. prendre 1000000. déposer 500 $ cycle [1..20] . Ce qui fera la somme des 1 000 000 numéros, même dans une boucle plutôt que dans une liste; Néanmoins, il devrait être réduit à un seul calcul numérique direct, avec peu de conditions et peu de formules. Ce qui serait beaucoup mieux alors de résumer les 1 000 000 numéros. Même s’il s’agit d’une boucle et non d’une liste (c’est-à-dire après l’optimisation de la déforestation).

Autre chose, cela permet de coder en le retour en arrière modulo contre style, et il fonctionne juste .

cf. réponse associée .

Si par "évaluation paresseuse" vous voulez dire comme dans les combats booléens, comme dans

   if (ConditionA && ConditionB) ... 

alors la réponse est simplement que moins le processeur consomme de cycle de traitement, plus il fonctionnera vite ... et si un bloc d'instructions de traitement n'aura aucun impact sur le résultat du programme, il est inutile (et donc une perte de temps) pour les exécuter quand même ...

si otoh, vous voulez dire ce que j'ai appelé "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)); 
          } 
    }
}

Cette technique permet au code client d’utiliser la classe pour éviter d’appeler la base de données pour l’enregistrement de superviseur, sauf lorsque le client utilisant l’objet Employé requiert l’accès aux données du superviseur ... ce qui rend le processus d’instanciation d’un Employé plus rapidement et pourtant, lorsque vous avez besoin du superviseur, le premier appel à la propriété Superviseur déclenchera l'appel de la base de données et les données seront récupérées et disponibles ...

Extrait des Fonctions d'ordre supérieur

  

Trouvons le plus grand nombre sous 100 000 divisible par 3829.   Pour ce faire, nous allons simplement filtrer un ensemble de possibilités dans lesquelles nous savons   la solution réside.

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

Nous faisons d’abord une liste décroissante de tous les nombres inférieurs à 100 000.   Ensuite, nous filtrons par notre prédicat et parce que les nombres sont triés   de manière descendante, 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   besoin d'utiliser une liste finie pour notre ensemble de départ. C'est la paresse dans   action à nouveau. Parce que nous finissons seulement par utiliser la tête du filtre   liste, peu importe que la liste filtrée soit finie ou infinie.   L'évaluation s'arrête lorsque la première solution adéquate est trouvée.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top