Mi chiedo da tempo perché la valutazione pigra sia utile. Devo ancora farmi spiegare da nessuno in un modo che abbia senso; per lo più finisce per ridursi a "fidati di me".
Nota: non intendo memoizzazione.
Mi chiedo da tempo perché la valutazione pigra sia utile. Devo ancora farmi spiegare da nessuno in un modo che abbia senso; per lo più finisce per ridursi a "fidati di me".
Nota: non intendo memoizzazione.
Risposte:
Principalmente perché può essere più efficiente: i valori non devono essere calcolati se non verranno utilizzati. Ad esempio, posso passare tre valori in una funzione, ma a seconda della sequenza di espressioni condizionali, può essere effettivamente utilizzato solo un sottoinsieme. In un linguaggio come il C, tutti e tre i valori sarebbero comunque calcolati; ma in Haskell vengono calcolati solo i valori necessari.
Consente anche cose interessanti come elenchi infiniti. Non posso avere una lista infinita in una lingua come C, ma in Haskell, non è un problema. Gli elenchi infiniti sono usati abbastanza spesso in alcune aree della matematica, quindi può essere utile avere la capacità di manipolarli.
Un utile esempio di valutazione pigra è l'uso di quickSort
:
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
Se ora vogliamo trovare il minimo della lista, possiamo definire
minimum ls = head (quickSort ls)
Che prima ordina l'elenco e quindi prende il primo elemento dell'elenco. Tuttavia, a causa della valutazione lenta, viene calcolata solo la testa. Ad esempio, se prendiamo il minimo della lista, [2, 1, 3,]
quickSort filtrerà prima tutti gli elementi che sono più piccoli di due. Quindi esegue quickSort su questo (restituendo l'elenco singleton [1]) che è già sufficiente. A causa della valutazione pigra, il resto non viene mai ordinato, risparmiando molto tempo di calcolo.
Questo è ovviamente un esempio molto semplice, ma la pigrizia funziona allo stesso modo per i programmi che sono molto grandi.
C'è, tuttavia, uno svantaggio in tutto questo: diventa più difficile prevedere la velocità di runtime e l'utilizzo della memoria del programma. Ciò non significa che i programmi pigri siano più lenti o richiedano più memoria, ma è bene saperlo.
take k $ quicksort list
richiede solo il tempo O (n + k log k), dove n = length list
. Con un ordinamento di confronto non pigro, ciò richiederebbe sempre O (n log n) tempo.
Trovo che la valutazione pigra sia utile per una serie di cose.
In primo luogo, tutte le lingue pigre esistenti sono pure, perché è molto difficile ragionare sugli effetti collaterali in una lingua pigra.
I linguaggi puri consentono di ragionare sulle definizioni di funzioni utilizzando il ragionamento equazionale.
foo x = x + 3
Sfortunatamente in un'impostazione non pigra, non vengono restituite più istruzioni rispetto a un'impostazione pigra, quindi questo è meno utile in linguaggi come ML. Ma in un linguaggio pigro puoi tranquillamente ragionare sull'uguaglianza.
In secondo luogo, molte cose come la "restrizione del valore" in ML non sono necessarie in linguaggi pigri come Haskell. Questo porta a un grande decluttering della sintassi. I linguaggi simili al ML devono utilizzare parole chiave come var o fun. In Haskell queste cose si riducono a una nozione.
Terzo, la pigrizia ti consente di scrivere codice molto funzionale che può essere compreso a pezzi. In Haskell è comune scrivere un corpo di funzione come:
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 = ...
Ciò consente di lavorare "dall'alto verso il basso" attraverso la comprensione del corpo di una funzione. I linguaggi simili a ML ti costringono a utilizzare un valore let
valutato rigorosamente. Di conseguenza, non si osa "sollevare" la clausola let al corpo principale della funzione, perché se è costosa (o ha effetti collaterali) non si vuole che venga sempre valutata. Haskell può "trasferire" i dettagli alla clausola where in modo esplicito perché sa che il contenuto di quella clausola verrà valutato solo se necessario.
In pratica, tendiamo a usare le protezioni e le crolliamo ulteriormente per:
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
Quarto, la pigrizia a volte offre un'espressione molto più elegante di certi algoritmi. Un "ordinamento rapido" pigro in Haskell è una riga e ha il vantaggio che se guardi solo i primi articoli, paghi solo i costi proporzionali al costo della selezione di quegli elementi. Niente ti impedisce di farlo in modo rigoroso, ma probabilmente dovrai ricodificare l'algoritmo ogni volta per ottenere le stesse prestazioni asintotiche.
In quinto luogo, la pigrizia ti consente di definire nuove strutture di controllo nella lingua. Non puoi scrivere un nuovo costrutto tipo "se .. allora .. altro .." in un linguaggio rigoroso. Se provi a definire una funzione come:
if' True x y = x
if' False x y = y
in un linguaggio rigoroso, entrambi i rami sarebbero valutati indipendentemente dal valore della condizione. La situazione peggiora se si considerano i loop. Tutte le soluzioni rigorose richiedono che il linguaggio fornisca una sorta di citazione o una costruzione lambda esplicita.
Infine, nello stesso spirito, alcuni dei migliori meccanismi per trattare gli effetti collaterali nel sistema dei tipi, come le monadi, possono davvero essere espressi efficacemente solo in un ambiente pigro. Ciò può essere verificato confrontando la complessità dei flussi di lavoro di F # con le monadi Haskell. (Puoi definire una monade in un linguaggio rigoroso, ma sfortunatamente spesso fallirai una o due leggi della monade a causa della mancanza di pigrizia e i flussi di lavoro al confronto raccolgono una tonnellata di bagagli rigorosi.)
let
è una bestia pericolosa, nello schema R6RS fa #f
apparire casuali nel tuo termine ovunque legare il nodo conduca strettamente a un ciclo! Nessun gioco di parole, ma let
legami strettamente più ricorsivi sono sensati in un linguaggio pigro. La severità esacerba anche il fatto che where
non ha alcun modo di ordinare gli effetti relativi, tranne che per SCC, è una costruzione a livello di istruzione, i suoi effetti potrebbero verificarsi in qualsiasi ordine rigorosamente, e anche se hai un linguaggio puro finisci con il #f
problema. Rigorosi where
enigmi il tuo codice con preoccupazioni non locali.
ifFunc(True, x, y)
valuterà sia x
e y
invece che solo x
.
C'è una differenza tra la valutazione dell'ordine normale e una valutazione pigra (come in Haskell).
square x = x * x
Valutazione della seguente espressione ...
square (square (square 2))
... con un'attenta valutazione:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
... con normale valutazione dell'ordine:
> (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
... con valutazione pigra:
> (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
Questo perché la valutazione pigra guarda l'albero della sintassi e fa trasformazioni ad albero ...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
... mentre la valutazione dell'ordine normale fa solo espansioni testuali.
Ecco perché noi, quando usiamo la valutazione pigra, diventiamo più potenti (la valutazione termina più spesso di altre strategie) mentre la prestazione è equivalente alla valutazione avida (almeno nella notazione O).
Valutazione pigra relativa alla CPU allo stesso modo della garbage collection relativa alla RAM. GC ti consente di fingere di avere una quantità illimitata di memoria e quindi richiedere tutti gli oggetti in memoria di cui hai bisogno. Il runtime recupererà automaticamente gli oggetti inutilizzabili. LE ti consente di fingere di avere risorse computazionali illimitate: puoi fare tutti i calcoli di cui hai bisogno. Il runtime semplicemente non eseguirà calcoli non necessari (per un dato caso).
Qual è il vantaggio pratico di questi modelli "finti"? Rilascia lo sviluppatore (in una certa misura) dalla gestione delle risorse e rimuove parte del codice boilerplate dai sorgenti. Ma la cosa più importante è che puoi riutilizzare in modo efficiente la tua soluzione in un insieme più ampio di contesti.
Immagina di avere una lista di numeri S e un numero N. Devi trovare il più vicino al numero N numero M dalla lista S. Puoi avere due contesti: la singola N e qualche lista L di Ns (ad esempio per ogni N in L cerchi la M più vicina in S). Se usi la valutazione pigra, puoi ordinare S e applicare la ricerca binaria per trovare la M più vicina a N. Per un buon ordinamento pigro richiederà O (size (S)) passaggi per i singoli N e O (ln (size (S)) * (size (S) + size (L))) passi per L. equamente distribuito Se non si dispone di una valutazione pigra per ottenere l'efficienza ottimale, è necessario implementare l'algoritmo per ogni contesto.
Se credi a Simon Peyton Jones, la valutazione pigra non è importante di per sé ma solo come una "camicia per capelli" che ha costretto i designer a mantenere puro il linguaggio. Mi trovo in sintonia con questo punto di vista.
Richard Bird, John Hughes e, in misura minore, Ralf Hinze sono in grado di fare cose incredibili con una valutazione pigra. Leggere il loro lavoro ti aiuterà ad apprezzarlo. Un buon punto di partenza sono il magnifico risolutore di Sudoku di Bird e l'articolo di Hughes su Why Functional Programming Matters .
IO
monade) la firma di main
sarebbe stata String -> String
e si poteva già scrivere programmi adeguatamente interattivi.
IO
monade?
Considera un programma tris. Ha quattro funzioni:
Questo crea una bella netta separazione delle preoccupazioni. In particolare la funzione di generazione delle mosse e le funzioni di valutazione della scacchiera sono le uniche che necessitano di comprendere le regole del gioco: le funzioni albero delle mosse e minimax sono completamente riutilizzabili.
Ora proviamo a implementare gli scacchi invece del tris. In un linguaggio "desideroso" (cioè convenzionale) questo non funzionerà perché l'albero di spostamento non si adatterà alla memoria. Quindi ora le funzioni di valutazione della scheda e generazione di mosse devono essere combinate con l'albero di movimento e la logica minimax perché la logica minimax deve essere utilizzata per decidere quali mosse generare. La nostra bella struttura modulare pulita scompare.
Tuttavia in un linguaggio pigro gli elementi dell'albero di spostamento sono generati solo in risposta alle richieste della funzione minimax: non è necessario generare l'intero albero di spostamento prima di lasciare che minimax si liberi sull'elemento superiore. Quindi la nostra struttura modulare pulita funziona ancora in un gioco reale.
Qui ci sono altri due punti che non credo siano stati ancora sollevati nella discussione.
La pigrizia è un meccanismo di sincronizzazione in un ambiente simultaneo. È un modo semplice e leggero per creare un riferimento a un calcolo e condividerne i risultati tra molti thread. Se più thread tentano di accedere a un valore non valutato, solo uno di essi lo eseguirà e gli altri si bloccheranno di conseguenza, ricevendo il valore una volta che diventa disponibile.
La pigrizia è fondamentale per ammortizzare le strutture dati in un contesto puro. Questo è descritto in dettaglio da Okasaki in Strutture dati puramente funzionali , ma l'idea di base è che la valutazione pigra è una forma controllata di mutazione critica per permetterci di implementare alcuni tipi di strutture dati in modo efficiente. Mentre spesso parliamo di pigrizia che ci costringe a indossare la maglietta della purezza, vale anche l'altro modo: sono un paio di caratteristiche linguistiche sinergiche.
Quando accendi il tuo computer e Windows si astiene dall'aprire ogni singola directory sul tuo disco rigido in Windows Explorer e si astiene dal lanciare ogni singolo programma installato sul tuo computer, fino a quando non indichi che è necessaria una certa directory o è necessario un certo programma, che è una valutazione "pigra".
La valutazione "pigra" esegue le operazioni quando e come sono necessarie. È utile quando è una caratteristica di un linguaggio di programmazione o di una libreria perché è generalmente più difficile implementare una valutazione pigra da soli che semplicemente precalcolare tutto in anticipo.
Considera questo:
if (conditionOne && conditionTwo) {
doSomething();
}
Il metodo doSomething () verrà eseguito solo se conditionOne è vera e conditionTwo è vera. Nel caso in cui conditionOne sia false, perché è necessario calcolare il risultato di conditionTwo? La valutazione di conditionTwo sarà una perdita di tempo in questo caso, soprattutto se la tua condizione è il risultato di un processo metodologico.
Questo è un esempio dell'interesse per la valutazione pigra ...
Può aumentare l'efficienza. Questo è quello che sembra ovvio, ma in realtà non è il più importante. (Nota anche che la pigrizia può anche uccidere l' efficienza: questo fatto non è immediatamente ovvio. Tuttavia, memorizzando molti risultati temporanei anziché calcolarli immediatamente, puoi utilizzare un'enorme quantità di RAM.)
Ti consente di definire i costrutti di controllo del flusso nel normale codice a livello di utente, invece di essere hardcoded nel linguaggio. (Ad esempio, Java ha for
loop; Haskell ha una for
funzione. Java ha la gestione delle eccezioni; Haskell ha vari tipi di monade di eccezioni. C # ha goto
; Haskell ha la monade di continuazione ...)
Ti consente di disaccoppiare l'algoritmo per la generazione dei dati dall'algoritmo per decidere la quantità di dati da generare. È possibile scrivere una funzione che generi un elenco di risultati teoricamente infinito e un'altra funzione che elabori la maggior parte di questo elenco in base alle sue esigenze. Più precisamente, puoi avere cinque funzioni del generatore e cinque funzioni del consumatore e puoi produrre in modo efficiente qualsiasi combinazione, invece di codificare manualmente 5 x 5 = 25 funzioni che combinano entrambe le azioni contemporaneamente. (!) Sappiamo tutti che il disaccoppiamento è una buona cosa.
Più o meno ti costringe a progettare un linguaggio funzionale puro . È sempre forte la tentazione di prendere scorciatoie , ma in un linguaggio pigro, la minima impurità rende il tuo codice selvaggiamente imprevedibile, il che milita fortemente contro le scorciatoie.
Un enorme vantaggio della pigrizia è la capacità di scrivere strutture di dati immutabili con limiti ammortizzati ragionevoli. Un semplice esempio è uno stack immutabile (utilizzando 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)
Il codice è ragionevole, ma l'aggiunta di due stack xey richiede tempo O (lunghezza di x) nei casi migliori, peggiori e medi. L'aggiunta di due stack è un'operazione monolitica, tocca tutti i nodi nello stack x.
Possiamo riscrivere la struttura dei dati come uno stack pigro:
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
funziona sospendendo la valutazione del codice nel suo costruttore. Una volta valutato utilizzando .Force()
, il valore restituito viene memorizzato nella cache e riutilizzato ogni successivo .Force()
.
Con la versione pigra, gli appendi sono un'operazione O (1): restituisce 1 nodo e sospende la ricostruzione effettiva della lista. Quando ottieni il capo di questa lista, valuterà il contenuto del nodo, costringendolo a restituire la testa e creare una sospensione con gli elementi rimanenti, quindi prendere il capo della lista è un'operazione O (1).
Quindi, la nostra lista pigra è in uno stato costante di ricostruzione, non paghi il costo per la ricostruzione di questa lista finché non attraversi tutti i suoi elementi. Usando la pigrizia, questo elenco supporta O (1) consing e aggiunta. È interessante notare che, poiché non valutiamo i nodi fino al loro accesso, è del tutto possibile costruire un elenco con elementi potenzialmente infiniti.
La struttura dati sopra non richiede il ricalcolo dei nodi su ogni attraversamento, quindi sono nettamente diversi da IEnumerables vanilla in .NET.
Questo frammento mostra la differenza tra valutazione pigra e non pigra. Ovviamente questa funzione di Fibonacci potrebbe essere ottimizzata e utilizzare la valutazione pigra invece della ricorsione, ma ciò rovinerebbe l'esempio.
Supponiamo di POSSIAMO dover usare i primi 20 numeri per qualcosa, non con la valutazione pigra tutti i 20 numeri devono essere generati in anticipo, ma, con la valutazione pigra essi saranno generati in base alle esigenze solo. Quindi pagherai solo il prezzo di calcolo quando necessario.
Output di esempio
Generazione non pigra: 0,023373 Generazione pigra: 0,000009 Uscita non pigra: 0.000921 Uscita pigra: 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)
La valutazione pigra è molto utile con le strutture dati. È possibile definire un array o un vettore specificando induttivamente solo alcuni punti nella struttura ed esprimendo tutti gli altri in termini dell'intero array. Ciò consente di generare strutture dati in modo molto conciso e con prestazioni di runtime elevate.
Per vederlo in azione, puoi dare un'occhiata alla mia libreria di rete neurale chiamata istinto . Fa un uso massiccio della valutazione pigra per l'eleganza e le alte prestazioni. Ad esempio, mi sbarazzo completamente del calcolo di attivazione tradizionalmente imperativo. Una semplice espressione pigra fa tutto per me.
Questo viene utilizzato ad esempio nella funzione di attivazione e anche nell'algoritmo di apprendimento della propagazione (posso pubblicare solo due collegamenti, quindi dovrai cercare tu stesso la learnPat
funzione nel AI.Instinct.Train.Delta
modulo). Tradizionalmente entrambi richiedono algoritmi iterativi molto più complicati.
Altre persone hanno già fornito tutte le ragioni principali, ma penso che un esercizio utile per aiutare a capire perché la pigrizia è importante è provare a scrivere una funzione a punto fisso in un linguaggio rigoroso.
In Haskell, una funzione a punto fisso è semplicissima:
fix f = f (fix f)
questo si espande a
f (f (f ....
ma poiché Haskell è pigro, quella catena infinita di calcoli non è un problema; la valutazione viene eseguita "dall'esterno all'interno" e tutto funziona a meraviglia:
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
È importante sottolineare che non importa fix
essere pigri, ma f
essere pigri. Una volta che ti è già stato assegnato un rigoroso f
, puoi alzare le mani in aria e arrenderti, oppure espanderlo e ingombrare le cose. (Questo è molto simile a quello che diceva Noah sul fatto che fosse la libreria rigorosa / pigra, non la lingua).
Ora immagina di scrivere la stessa funzione in Scala rigorosa:
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)
}
Ovviamente ottieni un overflow dello stack. Se vuoi che funzioni, devi rendere l' f
argomento 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)
Non so come pensi le cose attualmente, ma trovo utile pensare alla valutazione pigra come a un problema di libreria piuttosto che a una caratteristica del linguaggio.
Voglio dire che nei linguaggi rigorosi, posso implementare la valutazione pigra costruendo alcune strutture di dati e nei linguaggi pigri (almeno Haskell), posso chiedere rigore quando lo voglio. Pertanto, la scelta della lingua non rende i tuoi programmi pigri o non pigri, ma influisce semplicemente su quello che ottieni di default.
Una volta che ci pensi in questo modo, pensa a tutti i posti in cui scrivi una struttura dati che puoi utilizzare in seguito per generare dati (senza guardarli troppo prima di allora) e vedrai molti usi per pigri valutazione.
Lo sfruttamento più utile della valutazione pigra che ho usato è stata una funzione che chiamava una serie di sotto-funzioni in un ordine particolare. Se una qualsiasi di queste sotto-funzioni falliva (restituiva false), la funzione chiamante doveva tornare immediatamente. Quindi avrei potuto farlo in questo modo:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
oppure, la soluzione più elegante:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
Una volta che inizi a usarlo, vedrai opportunità per usarlo sempre più spesso.
Senza una valutazione pigra non ti sarà permesso di scrivere qualcosa del genere:
if( obj != null && obj.Value == correctValue )
{
// do smth
}
Tra le altre cose, i linguaggi pigri consentono strutture di dati infinite multidimensionali.
Sebbene lo schema, il python e così via consentano strutture dati infinite monodimensionali con flussi, è possibile attraversare solo una dimensione.
La pigrizia è utile per lo stesso problema marginale , ma vale la pena notare la connessione coroutine menzionata in quel link.
La valutazione pigra è il ragionamento equazionale dei poveri (che ci si potrebbe aspettare, idealmente, di dedurre le proprietà del codice dalle proprietà dei tipi e delle operazioni coinvolte).
Esempio in cui funziona abbastanza bene: sum . take 10 $ [1..10000000000]
. Che non ci dispiace essere ridotti a una somma di 10 numeri, invece di un solo calcolo numerico diretto e semplice. Senza la valutazione pigra, ovviamente, questo creerebbe un gigantesco elenco in memoria solo per utilizzare i suoi primi 10 elementi. Sarebbe sicuramente molto lento e potrebbe causare un errore di memoria insufficiente.
Esempio dove non è così grande come vorremmo: sum . take 1000000 . drop 500 $ cycle [1..20]
. Che in realtà sommerà i 1 000 000 di numeri, anche se in un ciclo invece che in un elenco; tuttavia dovrebbe essere ridotto a un solo calcolo numerico diretto, con pochi condizionali e poche formule. Il che sarebbe molto meglio poi sommando i 1 000 000 numeri. Anche se in un ciclo, e non in una lista (cioè dopo l'ottimizzazione della deforestazione).
Un'altra cosa è che rende possibile codificare in ricorsione in coda modulo cons style, e funziona .
cf. risposta correlata .
Se per "valutazione pigra" intendi come in booleani combinati, come in
if (ConditionA && ConditionB) ...
allora la risposta è semplicemente che minore è il numero di cicli di CPU consumati dal programma, più velocemente verrà eseguito ... e se una parte delle istruzioni di elaborazione non avrà alcun impatto sul risultato del programma, allora non è necessario, (e quindi uno spreco di tempo) per eseguirli comunque ...
se otoh, intendi quelli che ho conosciuto come "inizializzatori pigri", come in:
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));
}
}
}
Bene, questa tecnica consente al codice client di utilizzare la classe per evitare la necessità di chiamare il database per il record di dati del supervisore tranne quando il client che utilizza l'oggetto Employee richiede l'accesso ai dati del supervisore ... questo rende il processo di istanziazione di un dipendente più veloce, e tuttavia quando hai bisogno del Supervisore, la prima chiamata alla proprietà Supervisor attiverà la chiamata del database ei dati saranno recuperati e disponibili ...
Estratto dalle funzioni di ordine superiore
Troviamo il numero più grande sotto 100.000 divisibile per 3829. Per farlo, filtreremo semplicemente un insieme di possibilità in cui sappiamo che si trova la soluzione.
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
Per prima cosa creiamo un elenco di tutti i numeri inferiori a 100.000, in ordine decrescente. Quindi lo filtriamo in base al nostro predicato e poiché i numeri sono ordinati in modo decrescente, il numero più grande che soddisfa il nostro predicato è il primo elemento dell'elenco filtrato. Non avevamo nemmeno bisogno di usare un elenco finito per il nostro set di partenza. Questa è di nuovo la pigrizia in azione. Poiché finiamo per utilizzare solo l'inizio dell'elenco filtrato, non importa se l'elenco filtrato è finito o infinito. La valutazione si interrompe quando viene trovata la prima soluzione adeguata.