Perché è utile la valutazione pigra?


119

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:


96

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.


6
Python ha elenchi infiniti valutati pigramente tramite iteratori
Mark Cidade,

4
Puoi effettivamente emulare una lista infinita in Python usando generatori ed espressioni del generatore (che funzionano in modo simile alla comprensione di una lista): python.org/doc/2.5.2/ref/genexpr.html
John Montgomery,

24
I generatori rendono facili le liste pigre in Python, ma altre tecniche di valutazione pigre e strutture dati sono notevolmente meno eleganti.
Peter Burns,

3
Temo di non essere d'accordo con questa risposta. Pensavo che la pigrizia fosse una questione di efficienza, ma avendo usato Haskell una quantità notevole, e poi passando a Scala e confrontando l'esperienza, devo dire che la pigrizia conta spesso ma raramente a causa dell'efficienza. Penso che Edward Kmett abbia centrato le vere ragioni.
Owen

3
Allo stesso modo non sono d'accordo, anche se non esiste una nozione esplicita di un elenco infinito in C a causa della valutazione rigorosa, puoi facilmente giocare lo stesso trucco in qualsiasi altra lingua (e in effetti, nella maggior parte dell'implementazione effettiva di ogni lingua pigra) usando thunk e passando la funzione puntatori per lavorare con un prefisso finito della struttura infinita prodotta da espressioni simili.
Kristopher Micinski

71

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.


19
Più in generale, take k $ quicksort listrichiede 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.
effimero

@ephemient non intendi O (nk log k)?
MaiaVictor

1
@Viclib No, intendevo quello che ho detto.
effimero

@ephemient quindi penso di non
capirlo

2
@Viclib Un algoritmo di selezione per trovare i primi k elementi di n è O (n + k log k). Quando si implementa il quicksort in un linguaggio pigro e lo si valuta solo abbastanza lontano da determinare i primi k elementi (interrompendo la valutazione dopo), esegue esattamente gli stessi confronti di un algoritmo di selezione non pigro.
effimero

70

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


5
Molto bella; queste sono le vere risposte. Pensavo che si trattasse di efficienza (ritardare i calcoli per dopo) finché non ho usato Haskell una quantità significativa e ho visto che non è proprio questo il motivo.
Owen

11
Inoltre, sebbene non sia tecnicamente vero che un linguaggio pigro debba essere puro (R come esempio), è vero che un linguaggio pigro impuro può fare cose molto strane (R come esempio).
Owen

4
Certo che c'è. In un linguaggio rigoroso il ricorsivo letè una bestia pericolosa, nello schema R6RS fa #fapparire casuali nel tuo termine ovunque legare il nodo conduca strettamente a un ciclo! Nessun gioco di parole, ma letlegami strettamente più ricorsivi sono sensati in un linguaggio pigro. La severità esacerba anche il fatto che wherenon 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 #fproblema. Rigorosi whereenigmi il tuo codice con preoccupazioni non locali.
Edward KMETT

2
Puoi spiegare in che modo la pigrizia aiuta a evitare la restrizione del valore? Non sono stato in grado di capirlo.
Tom Ellis

3
@PaulBone Di cosa stai parlando? La pigrizia ha molto a che fare con le strutture di controllo. Se definisci la tua struttura di controllo in un linguaggio rigoroso, dovrai usare un mucchio di lambda o simili, o farà schifo. Perché ifFunc(True, x, y)valuterà sia xe yinvece che solo x.
punto

28

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


25

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.


L'analogia con il GC mi ha aiutato un po ', ma puoi fare un esempio di "rimuove alcuni codici standard" per favore?
Abdul

1
@Abdul, un esempio familiare a qualsiasi utente ORM: caricamento lazy dell'associazione. Carica l'associazione dal DB "just in time" e allo stesso tempo libera uno sviluppatore dalla necessità di specificare esplicitamente quando caricarlo e come memorizzarlo nella cache (questo è il boilerplate intendo). Ecco un altro esempio: projectlombok.org/features/GetterLazy.html .
Alexey

25

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 .


Non solo li costringeva a mantenere il linguaggio puro, ma gli permetteva anche di farlo, quando (prima dell'introduzione della IOmonade) la firma di mainsarebbe stata String -> Stringe si poteva già scrivere programmi adeguatamente interattivi.
sinistra intorno

@leftaroundabout: Cosa impedisce a un linguaggio rigoroso di forzare tutti gli effetti in una IOmonade?
Tom Ellis

13

Considera un programma tris. Ha quattro funzioni:

  • Una funzione di generazione di mosse che prende una tavola corrente e genera un elenco di nuove tavole ciascuna con una mossa applicata.
  • Poi c'è una funzione "move tree" che applica la funzione di generazione di mosse per derivare tutte le possibili posizioni della scheda che potrebbero seguire da questa.
  • C'è una funzione minimax che percorre l'albero (o forse solo una parte di esso) per trovare la migliore mossa successiva.
  • C'è una funzione di valutazione del tabellone che determina se uno dei giocatori ha vinto.

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.


1
[In un linguaggio "desideroso" (cioè convenzionale) questo non funzionerà perché l'albero dei movimenti non si adatterà alla memoria] - per Tic-Tac-Toe lo farà sicuramente. Ci sono al massimo 3 ** 9 = 19683 posizioni da memorizzare. Se memorizziamo ciascuno di essi in uno stravagante 50 byte, è quasi un megabyte. Non è niente ...
Jonas Kölker

6
Sì, questo è il mio punto. Le lingue desiderose possono avere una struttura pulita per giochi banali, ma devono comprometterla per qualcosa di reale. Le lingue pigre non hanno questo problema.
Paul Johnson,

3
Per essere onesti, tuttavia, una valutazione pigra può portare a problemi di memoria. Non è raro che le persone si chiedano perché haskell sta soffiando via la sua memoria per qualcosa che, in una valutazione
avida

@PaulJohnson Se valuti tutte le posizioni, non fa differenza se le valuti avidamente o pigramente. Lo stesso lavoro deve essere fatto. Se ti fermi a metà e valuti solo la metà delle posizioni, anche questo non fa alcuna differenza, perché in entrambi i casi la metà del lavoro va fatta. L'unica differenza tra le due valutazioni è che l'algoritmo ha un aspetto più gradevole, se scritto pigramente.
ceving

12

Qui ci sono altri due punti che non credo siano stati ancora sollevati nella discussione.

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

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


10

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.


1
Alcune persone potrebbero dire che questa è davvero "un'esecuzione pigra". La differenza è davvero irrilevante tranne che in linguaggi ragionevolmente puri come Haskell; ma la differenza è che non si tratta solo di ritardi nel calcolo, ma anche di effetti collaterali ad esso associati (come l'apertura e la lettura di file).
Owen

8

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


Ho pensato che fosse un cortocircuito, non una valutazione pigra.
Thomas Owens,

2
È una valutazione pigra in quanto conditionTwo viene calcolato solo se è realmente necessario (cioè se conditionOne è vero).
Romain Linsolas,

7
Suppongo che il cortocircuito sia un caso degenerato di valutazione pigra, ma sicuramente non è un modo comune di pensarci.
rmeador

19
Il cortocircuito è infatti un caso speciale di valutazione pigra. La valutazione pigra comprende molto più del semplice cortocircuito, ovviamente. Oppure, cosa ha il cortocircuito oltre alla valutazione pigra?
yfeldblum

2
@ Juliet: hai una forte definizione di "pigro". Il tuo esempio di una funzione che prende due parametri non è lo stesso di un'istruzione if di cortocircuito. Un'istruzione if in cortocircuito evita calcoli non necessari. Penso che un confronto migliore con il tuo esempio sarebbe l'operatore di Visual Basic "andalso" che forza la valutazione di entrambe le condizioni

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

  2. 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 forloop; Haskell ha una forfunzione. Java ha la gestione delle eccezioni; Haskell ha vari tipi di monade di eccezioni. C # ha goto; Haskell ha la monade di continuazione ...)

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

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


6

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

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


5

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)

5

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 learnPatfunzione nel AI.Instinct.Train.Deltamodulo). Tradizionalmente entrambi richiedono algoritmi iterativi molto più complicati.


4

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 fixessere pigri, ma fessere 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' fargomento 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)

3

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.


1
L'implementazione di una valutazione pigra in linguaggi rigorosi è spesso un Turing Tarpit.
itsbruce

2

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.


2

Senza una valutazione pigra non ti sarà permesso di scrivere qualcosa del genere:

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

Bene, imo, questa è una cattiva idea farlo. Sebbene questo codice possa essere corretto (a seconda di ciò che si tenta di ottenere), è difficile da leggere, il che è sempre una cosa negativa.
Brann

12
Non credo proprio. È una costruzione standard in C e dei suoi parenti.
Paul Johnson,

Questo è un esempio di valutazione di cortocircuito, non di valutazione pigra. O è effettivamente la stessa cosa?
RufusVS

2

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.


2

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 .


1

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


0

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.