Perché la valutazione pigra non viene utilizzata ovunque?


32

Ho appena imparato come funziona la valutazione pigra e mi chiedevo: perché la valutazione pigra non viene applicata in tutti i software attualmente prodotti? Perché usare ancora una valutazione entusiasta?


2
Ecco un esempio di cosa può accadere se si mescolano lo stato mutevole e la valutazione pigra. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström

2
@ JonasElfström: Per favore, non confondere lo stato mutevole con una delle sue possibili implementazioni. Lo stato mutabile può essere implementato usando un flusso di valori infinito e pigro. Quindi non hai il problema delle variabili mutabili.
Giorgio,

Nei linguaggi di programmazione imperativi, la "valutazione pigra" richiede uno sforzo consapevole da parte del programmatore. La programmazione generica in linguaggi imperativi lo ha reso facile, ma non sarà mai trasparente. La risposta all'altro lato della domanda fa emergere un'altra domanda: "Perché i linguaggi di programmazione funzionale non sono usati ovunque?", E la risposta attuale è semplicemente "no" come una questione di attualità.
rwong,

2
I linguaggi di programmazione funzionale non vengono utilizzati ovunque per lo stesso motivo per cui non utilizziamo i martelli sulle viti, non tutti i problemi possono essere facilmente espressi in un input funzionale -> output output, ad esempio la GUI è più adatta per essere espressa in modo imperativo .
ALXGTV,

Inoltre ci sono due classi di linguaggi di programmazione funzionale (o almeno entrambi dichiarano di essere funzionali), i linguaggi funzionali imperativi come Clojure, Scala e il dichiarativo come Haskell, OCaml.
ALXGTV,

Risposte:


38

La valutazione pigra richiede un sovraccarico di contabilità: devi sapere se è stato ancora valutato e cose del genere. La valutazione desiderosa viene sempre valutata, quindi non devi saperlo. Ciò è particolarmente vero in contesti simultanei.

In secondo luogo, è banale convertire una valutazione desiderosa in una valutazione pigra impacchettandola in un oggetto funzione da chiamare in seguito, se lo si desidera.

In terzo luogo, la valutazione pigra implica una perdita di controllo. E se valutassi pigramente la lettura di un file da un disco? O hai tempo? Non è accettabile

La valutazione desiderosa può essere più efficiente e più controllabile, ed è banalmente convertita in valutazione pigra. Perché vorresti una valutazione pigra?


10
Leggere pigramente un file dal disco in realtà è davvero pulito - per la maggior parte dei miei programmi e script semplici, quello di Haskell readFileè esattamente ciò di cui ho bisogno. Inoltre, la conversione da valutazione pigra a desiderosa è altrettanto banale.
Tikhon Jelvis,

3
Sono d'accordo con tutti voi tranne l'ultimo paragrafo. La valutazione pigra è più efficiente quando si verifica un'operazione a catena e può avere un maggiore controllo su quando sono effettivamente necessari i dati
texasbruce,

4
Le leggi sui funzioni vorrebbero parlarti di "perdita di controllo". Se si scrivono funzioni pure che operano su tipi di dati immutabili, la valutazione pigra è una manna dal cielo. Lingue come l'hashell sono fondamentalmente basate sul concetto di pigrizia. È ingombrante in alcune lingue, specialmente se mescolato con codice "non sicuro", ma stai facendo sembrare che la pigrizia sia pericolosa o cattiva per impostazione predefinita. È solo "pericoloso" nel codice pericoloso.
Sara

1
@DeadMG Non se ti preoccupi se il tuo codice termina o no ... Cosa head [1 ..]ti dà in un linguaggio puro valutato con entusiasmo, perché in Haskell dà 1?
punto

1
Per molte lingue, l'implementazione della valutazione lenta introdurrà quantomeno la complessità. Talvolta è necessaria una certa complessità e avere una valutazione pigra migliora l'efficienza complessiva, in particolare se ciò che viene valutato è solo condizionatamente necessario. Tuttavia, fatto male, può introdurre bug sottili o difficili da spiegare problemi di prestazioni dovuti a ipotesi errate durante la scrittura del codice. C'è un compromesso.
Berin Loritsch,

17

Principalmente perché il codice e lo stato pigro possono mescolarsi male e causare alcuni bug difficili da trovare. Se lo stato di un oggetto dipendente cambia, il valore dell'oggetto pigro può essere errato quando valutato. È molto meglio che il programmatore codifichi esplicitamente l'oggetto in modo che sia pigro quando sa che la situazione è appropriata.

In una nota a margine Haskell utilizza la valutazione Lazy per tutto. Ciò è possibile perché è un linguaggio funzionale e non utilizza lo stato (tranne in alcune circostanze eccezionali in cui sono chiaramente contrassegnati)


Sì, stato mutevole + valutazione pigra = morte. Penso che gli unici punti persi nella mia finale SICP siano stati l'uso set!di un pigro interprete di Scheme. > :(
Tikhon Jelvis il

3
"codice e stato pigro possono mescolarsi male": dipende davvero da come si implementa lo stato. Se lo implementi utilizzando variabili mutabili condivise e dipendi dall'ordine di valutazione affinché il tuo stato sia coerente, allora hai ragione.
Giorgio,

14

La valutazione pigra non è sempre migliore.

I vantaggi in termini di prestazioni della valutazione lenta possono essere eccezionali, ma non è difficile evitare la maggior parte delle valutazioni non necessarie in ambienti desiderosi, sicuramente pigre lo rendono facile e completo, ma raramente la valutazione non necessaria nel codice è un grosso problema.

La cosa positiva della valutazione pigra è quando ti permette di scrivere un codice più chiaro; ottenere il decimo primo filtrando un infinito elenco di numeri naturali e prendere il decimo elemento di tale elenco è uno dei modi più concisi e chiari di procedere: (pseudocodice)

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)

Credo che sarebbe abbastanza difficile esprimere le cose in modo così conciso senza pigrizia.

Ma la pigrizia non è la risposta a tutto. Per cominciare, la pigrizia non può essere applicata in modo trasparente in presenza di stato, e credo che la statualità non possa essere rilevata automaticamente (a meno che tu non stia lavorando, ad esempio, Haskell, quando lo stato è abbastanza esplicito). Quindi, nella maggior parte delle lingue, la pigrizia deve essere fatta manualmente, il che rende le cose meno chiare e quindi rimuove uno dei grandi vantaggi della valutazione pigra.

Inoltre, la pigrizia presenta degli svantaggi in termini di prestazioni, in quanto comporta un notevole sovraccarico nel mantenere le espressioni non valutate; usano l'archiviazione e sono più lenti a lavorare rispetto ai semplici valori. Non è raro scoprire che è necessario un codice entusiasta perché la versione pigra è un cane lento - ed a volte è difficile ragionare sulle prestazioni.

Dato che tende ad accadere, non esiste una strategia migliore in assoluto. Lazy è fantastico se riesci a scrivere codice migliore sfruttando infinite strutture di dati o altre strategie che ti consente di utilizzare, ma desiderare può essere più facile da ottimizzare.


Sarebbe possibile per un compilatore davvero intelligente mitigare in modo significativo il sovraccarico. o anche approfittare della pigrizia per ulteriori ottimizzazioni?
Tikhon Jelvis,

3

Ecco un breve confronto dei pro e dei contro della valutazione desiderosa e pigra:

  • Valutazione desiderosa:

    • Potenziale sovraccarico di cose inutilmente valutabili.

    • Valutazione rapida e senza ostacoli.

  • Valutazione pigra:

    • Nessuna valutazione non necessaria.

    • Spese generali di contabilità ad ogni utilizzo di un valore.

Quindi, se hai molte espressioni che non devono mai essere valutate, pigro è meglio; ma se non hai mai un'espressione che non ha bisogno di essere valutata, pigro è puro sovraccarico.

Ora diamo un'occhiata al software del mondo reale: quante funzioni che scrivi non richiedono una valutazione di tutti i loro argomenti? Soprattutto con le moderne funzioni brevi che fanno solo una cosa, la percentuale di funzioni che rientrano in questa categoria è molto bassa. Pertanto, la valutazione lenta introdurrebbe semplicemente il sovraccarico di contabilità per la maggior parte del tempo, senza la possibilità di salvare effettivamente nulla.

Di conseguenza, la valutazione pigra semplicemente non paga in media, la valutazione desiderosa è la soluzione migliore per il codice moderno.


1
"Spese generali di contabilità ad ogni uso di un valore.": Non penso che le spese generali di contabilità siano maggiori, per esempio, del controllo di riferimenti null in una lingua come Java. In entrambi i casi è necessario controllare un bit di informazioni (valutate / in sospeso rispetto a null / non null) e occorre farlo ogni volta che si utilizza un valore. Quindi sì, c'è un sovraccarico, ma è minimo.
Giorgio,

1
"Quante funzioni che scrivi non richiedono la valutazione di tutti i loro argomenti?": Questa è solo un'applicazione di esempio. Che dire delle strutture di dati ricorsive e infinite? Puoi implementarli con una valutazione entusiasta? Puoi usare gli iteratori, ma la soluzione non è sempre così concisa. Ovviamente probabilmente non ti perderai qualcosa che non hai mai avuto la possibilità di usare ampiamente.
Giorgio,

2
"Di conseguenza, la valutazione pigra semplicemente non paga in media, la valutazione desiderosa è la soluzione migliore per il codice moderno.": Questa affermazione non regge: dipende davvero da ciò che stai cercando di implementare.
Giorgio,

1
@Giorgio Il sovraccarico potrebbe non sembrare molto per te, ma i condizionali sono una delle cose a cui le CPU moderne fanno schifo: un ramo mal previsto di solito forza un flusso completo della pipeline, gettando via il lavoro di più di dieci cicli di CPU. Non vuoi condizioni non necessarie nel tuo ciclo interiore. Pagare dieci cicli in più per argomento di funzione è quasi inaccettabile per il codice sensibile alle prestazioni come codificare la cosa in Java. Hai ragione che la valutazione pigra ti consente di trarre alcuni trucchi che non puoi facilmente fare con una valutazione entusiasta. Ma la stragrande maggioranza del codice non ha bisogno di questi trucchi.
cmaster

2
Questa sembra essere una risposta per inesperienza con le lingue con valutazione pigra. Ad esempio, che dire delle infinite strutture di dati?
Andres F.

3

Come notato da @DeadMG, la valutazione pigra richiede un sovraccarico di contabilità. Questo può essere costoso rispetto alla valutazione desiderosa. Considera questa affermazione:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Questo richiederà un po 'di calcolo per calcolare. Se utilizzo la valutazione lazy, devo verificare se è stata valutata ogni volta che la utilizzo. Se questo si trova all'interno di un circuito chiuso molto utilizzato, l'overhead aumenta in modo significativo, ma non ci sono vantaggi.

Con una valutazione entusiasta e un compilatore decente, la formula viene calcolata al momento della compilazione. La maggior parte degli ottimizzatori sposta l'assegnazione da eventuali loop in cui si verifica, se del caso.

La valutazione pigra è più adatta al caricamento di dati a cui si accede raramente e che ha un elevato sovraccarico da recuperare. È quindi più appropriato limitare i casi rispetto alla funzionalità principale.

In generale, è buona norma valutare il prima possibile le cose a cui si accede frequentemente. La valutazione pigra non funziona con questa pratica. Se accederai sempre a qualcosa, tutto ciò che la valutazione pigra farà è aggiungere un sovraccarico. Il costo / beneficio dell'utilizzo della valutazione lenta diminuisce man mano che si accede alla voce a cui si accede.

L'uso sempre della valutazione lenta implica anche un'ottimizzazione precoce. Questa è una cattiva pratica che spesso si traduce in un codice che è molto più complesso e costoso che altrimenti potrebbe essere il caso. Sfortunatamente, l'ottimizzazione prematura spesso porta a un codice che funziona più lentamente del codice più semplice. Fino a quando non puoi misurare l'effetto dell'ottimizzazione, è una cattiva idea ottimizzare il tuo codice.

Evitare l'ottimizzazione prematura non è in conflitto con le buone pratiche di codifica. Se non sono state applicate le buone pratiche, le ottimizzazioni iniziali possono consistere nell'applicazione di buone pratiche di codifica come lo spostamento dei calcoli dai cicli.


1
Sembra che tu stia litigando per inesperienza. Ti suggerisco di leggere l'articolo "Perché la programmazione funzionale conta" di Wadler. Dedica una sezione importante che spiega il perché della valutazione pigra (suggerimento: ha poco a che fare con le prestazioni, l'ottimizzazione precoce o il "caricamento di dati a cui si accede raramente" e tutto a che fare con la modularità).
Andres F.

@AndresF Ho letto il documento a cui ti riferisci. Sono d'accordo con l'uso della valutazione pigra in questi casi. La valutazione precoce potrebbe non essere appropriata, ma direi che la restituzione del sottoalbero per la mossa selezionata può avere un vantaggio significativo se si possono aggiungere facilmente mosse aggiuntive. Tuttavia, costruire tale funzionalità potrebbe essere un'ottimizzazione prematura. Al di fuori della programmazione funzionale, ho riscontrato problemi significativi con l'uso della valutazione pigra e il mancato utilizzo della valutazione pigra. Vi sono segnalazioni di significativi costi delle prestazioni derivanti dalla valutazione pigra nella programmazione funzionale.
BillThor l'

2
Ad esempio? Vi sono inoltre report di significativi costi delle prestazioni quando si utilizza anche una valutazione entusiasta (costi sotto forma di valutazione non necessaria, nonché di non terminazione del programma). Ci sono costi per quasi tutte le altre funzioni (usate), pensaci. La modularità stessa può avere un costo; il problema è se ne vale la pena.
Andres F.

3

Se dovessimo potenzialmente valutare completamente un'espressione per determinarne il valore, la valutazione lenta può essere uno svantaggio. Supponiamo di avere un lungo elenco di valori booleani e vogliamo scoprire se sono tutti veri:

[True, True, True, ... False]

Per fare ciò dobbiamo esaminare tutti gli elementi dell'elenco, non importa quale, quindi non c'è possibilità di interrompere pigramente la valutazione. Possiamo usare una piega per determinare se tutti i valori booleani nell'elenco sono veri. Se utilizziamo una piega a destra, che utilizza la valutazione lazy, non otteniamo nessuno dei vantaggi della valutazione lazy perché dobbiamo esaminare ogni elemento nell'elenco:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Una piega a destra sarà molto più lenta in questo caso di una piega rigorosa a sinistra, che non utilizza la valutazione pigra:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

Il motivo è che una piega rigorosa a sinistra utilizza la ricorsione della coda, il che significa che accumula il valore di ritorno e non accumula e memorizza in memoria una grande catena di operazioni. Questo è molto più veloce della piega pigra a destra perché entrambe le funzioni devono comunque guardare l'intero elenco e la piega a destra non può usare la ricorsione della coda. Quindi, il punto è che dovresti usare tutto ciò che è meglio per l'attività da svolgere.


"Quindi, il punto è che dovresti usare ciò che è meglio per il compito da svolgere." +1
Giorgio,
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.