Perché è utile il concetto di valutazione pigra?


30

Sembra che una valutazione pigra delle espressioni possa far perdere al programmatore il controllo sull'ordine in cui il suo codice viene eseguito. Ho difficoltà a capire perché questo sarebbe accettabile o desiderato da un programmatore.

Come può essere usato questo paradigma per costruire software prevedibile che funzioni come previsto, quando non abbiamo alcuna garanzia su quando e dove verrà valutata un'espressione?


10
Nella maggior parte dei casi non importa. Per tutti gli altri puoi semplicemente applicare la rigidità.
Cat Plus Plus,

22
Il punto di linguaggi puramente funzionali come haskell è che non devi preoccuparti quando il codice viene eseguito, poiché è privo di effetti collaterali.
Maschera di bit

21
Devi smettere di pensare a "eseguire il codice" e iniziare a pensare a "calcolare i risultati", perché è quello che vuoi davvero nei problemi più interessanti. Naturalmente i programmi di solito devono anche interagire con l'ambiente in qualche modo, ma ciò può spesso essere ridotto a una piccola parte del codice. Per il resto, puoi lavorare puramente funzionale e la pigrizia può rendere il ragionamento molto più semplice.
leftaroundabout

6
La domanda nel titolo ("Perché usare la valutazione pigra?") È molto diversa dalla domanda nel corpo ("Come usi la valutazione pigra?"). Per il primo, vedi la mia risposta a questa domanda correlata .
Daniel Wagner,

1
Un esempio quando pigrizia è utile: In Haskell head . sortha O(n)complessità per pigrizia (non O(n log n)). Vedi Valutazione pigra e complessità temporale .
Petr Pudlák,

Risposte:


62

Molte risposte vanno a cose come elenchi infiniti e guadagni in termini di prestazioni da parti del calcolo non valutate, ma a ciò manca la più grande motivazione per la pigrizia: la modularità .

Il classico argomento è esposto nel tanto citato documento "Why Functional Programming Matters" (collegamento PDF) di John Hughes. L'esempio chiave in quel documento (Sezione 5) sta giocando Tic-Tac-Toe usando l'algoritmo di ricerca alpha-beta. Il punto chiave è (p. 9):

[Valutazione pigra] rende pratico la modularizzazione di un programma come generatore che costruisce un gran numero di possibili risposte e un selettore che sceglie quello appropriato.

Il programma Tic-Tac-Toe può essere scritto come una funzione che genera l'intero albero di gioco a partire da una data posizione e una funzione separata che lo consuma. In fase di esecuzione ciò non genera intrinsecamente l'intero albero di gioco, ma solo quelle parti di cui il consumatore ha effettivamente bisogno. Possiamo cambiare l'ordine e la combinazione in cui vengono prodotte alternative cambiando il consumatore; non è necessario cambiare il generatore.

In una lingua entusiasta, non puoi scriverlo in questo modo perché probabilmente passeresti troppo tempo e memoria a generare l'albero. Quindi finisci o:

  1. Combinare generazione e consumo nella stessa funzione;
  2. Scrivere un produttore che funzioni in modo ottimale solo per determinati consumatori;
  3. Implementare la tua versione di pigrizia.

Per favore maggiori informazioni o un esempio. Sembra intrigante.
Alex Nye,

1
@AlexNye: il documento di John Hughes ha maggiori informazioni. Nonostante sia un documento accademico --- e quindi senza dubbio intimidatorio --- è in realtà molto accessibile e leggibile. Se non fosse per la sua lunghezza, probabilmente si adatterebbe come una risposta qui!
Tikhon Jelvis,

Forse per capire questa risposta, bisogna leggere il documento di Hughes ... non avendo fatto ciò, non riesco ancora a vedere come e perché la pigrizia e la modularità sono correlate.
stakx,

@stakx Senza una descrizione migliore, non sembrano essere collegati se non per caso. Il vantaggio della pigrizia in questo esempio è che un generatore pigro è in grado di generare tutti i possibili stati del gioco, ma non sta perdendo tempo / memoria a farlo perché verranno consumati solo quelli che accadono. Il generatore può essere separato dal consumatore senza essere un generatore pigro ed è possibile (sebbene più difficile) essere pigro senza essere separato dal consumatore.
Izkata,

@Iztaka: "Il generatore può essere separato dal consumatore senza essere un generatore pigro, ed è possibile (anche se più difficile) essere pigro senza essere separato dal consumatore." Nota che quando segui questa strada, potresti finire con un ** generatore eccessivamente specializzato ** - il generatore è stato scritto per ottimizzare un consumatore e quando riutilizzato per altri è subottimale. Un esempio comune sono i mappatori relazionali di oggetti che recuperano e costruiscono un intero grafico di oggetti solo perché si desidera una stringa dall'oggetto radice. La pigrizia evita molti di questi casi (ma non tutti).
Sacundim,

32

Come può essere usato questo paradigma per costruire software prevedibile che funzioni come previsto, quando non abbiamo alcuna garanzia su quando e dove verrà valutata un'espressione?

Quando un'espressione è priva di effetti collaterali, l'ordine in cui vengono valutate le espressioni non influisce sul loro valore, quindi il comportamento del programma non è influenzato dall'ordine. Quindi il comportamento è perfettamente prevedibile.

Ora gli effetti collaterali sono una questione diversa. Se gli effetti collaterali potessero verificarsi in qualsiasi ordine, il comportamento del programma sarebbe davvero imprevedibile. Ma questo non è in realtà il caso. Linguaggi pigri come Haskell rendono un punto di riferimento referenzialmente trasparente, ovvero assicurarsi che l'ordine in cui vengono valutate le espressioni non influenzerà mai il loro risultato. In Haskell ciò si ottiene forzando tutte le operazioni con effetti collaterali visibili dall'utente all'interno della monade IO. Questo assicura che tutti gli effetti collaterali si manifestino esattamente nell'ordine previsto.


15
Questo è il motivo per cui solo le lingue con "purezza forzata" come Haskell supportano la pigrizia ovunque per impostazione predefinita. Linguaggi di "purezza incoraggiata" come Scala richiedono al programmatore di dire esplicitamente dove vogliono la pigrizia, e spetta al programmatore assicurarsi che la pigrizia non causi problemi. Un linguaggio che aveva la pigrizia di default e aveva effetti collaterali non tracciati sarebbe davvero difficile da programmare in modo prevedibile.
Ben

1
sicuramente monadi diverse da IO possono anche causare effetti collaterali
jk.

1
@jk Solo IO può causare effetti collaterali esterni .
dave4420,

@ dave4420 sì, ma non è quello che dice questa risposta
jk.

1
@jk In Haskell attualmente no. Nessuna monade tranne IO (o quelle costruite su IO) ha effetti collaterali. E questo solo perché il compilatore tratta IO in modo diverso. Pensa a IO come "Immutable Off". Le monadi sono solo un modo (intelligente) per garantire un ordine di esecuzione specifico (quindi il tuo file verrà eliminato solo dopo che l'utente ha inserito "sì").
scarfridge,

22

Se hai familiarità con i database, un modo molto frequente per elaborare i dati è:

  • Fai una domanda come select * from foobar
  • Mentre ci sono più dati, fare: ottenere la riga successiva dei risultati ed elaborarli

Non si controlla come viene generato il risultato e in che modo (indici? Scansioni della tabella completa?) O quando (devono essere generati tutti i dati contemporaneamente o in modo incrementale quando viene richiesto?). Tutto quello che sai è: se ci sono più dati, li otterrai quando lo chiedi.

La valutazione pigra è abbastanza simile alla stessa cosa. Supponi di avere un elenco infinito definito come ie. la sequenza di Fibonacci - se hai bisogno di cinque numeri, ottieni cinque numeri calcolati; se hai bisogno di 1000 ottieni 1000. Il trucco è che il runtime sa cosa fornire dove e quando. È molto, molto utile.

(I programmatori Java possono emulare questo comportamento con Iteratori - altre lingue potrebbero avere qualcosa di simile)


Buon punto. Ad esempio Collection2.filter()(così come gli altri metodi di quella classe) implementa praticamente una valutazione pigra: il risultato "sembra" un ordinario Collection, ma l'ordine di esecuzione potrebbe essere non intuitivo (o almeno non ovvio). Inoltre, c'è yieldin Python (e una funzionalità simile in C #, di cui non ricordo il nome) che è ancora più vicino a supportare la valutazione pigra di un normale Iteratore.
Joachim Sauer

@JoachimSauer in C # il suo ritorno rendimento, o, naturalmente, è possibile utilizzare le oprerators LINQ precompilati, circa la metà dei quali sono pigri
JK.

+1: per menzionare gli iteratori in un linguaggio imperativo / orientato agli oggetti. Ho usato una soluzione simile per l'implementazione di stream e funzioni stream in Java. Usando gli iteratori potrei avere funzioni come take (n), dropWhile () su un flusso di input di lunghezza sconosciuta.
Giorgio,

13

Prendi in considerazione la possibilità di chiedere al tuo database un elenco dei primi 2000 utenti i cui nomi iniziano con "Ab" e hanno più di 20 anni. Inoltre devono essere maschi.

Ecco un piccolo diagramma.

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

Come puoi vedere da questa terribile e terribile interazione, il "database" in realtà non sta facendo nulla finché non è pronto a gestire tutte le condizioni. Risultati a caricamento lento ad ogni passaggio e applicazione di nuove condizioni ogni volta.

Al contrario di ottenere i primi 2000 utenti, restituendoli, filtrandoli per "Ab", restituendoli, filtrandoli per oltre 20, restituendoli e filtrando per maschio e infine restituendoli.

Caricamento pigro in breve.


1
questa è una spiegazione davvero scadente IMHO. Sfortunatamente non ho abbastanza rappresentante in questo particolare sito SE per votarlo. Il vero punto di valutazione pigra è che nessuno di questi risultati viene effettivamente prodotto fino a quando qualcos'altro è pronto a consumarli.
Alnitak,

La mia risposta postata dice esattamente la stessa cosa del tuo commento.
sergserg,

È un processore di programma molto educato.
Julian,

9

Una valutazione pigra delle espressioni farà perdere il controllo al progettista di un determinato pezzo di codice sulla sequenza in cui viene eseguito il codice.

Il progettista non dovrebbe preoccuparsi dell'ordine in cui vengono valutate le espressioni, a condizione che il risultato sia lo stesso. Differendo la valutazione, potrebbe essere possibile evitare di valutare del tutto alcune espressioni, risparmiando tempo.

Puoi vedere la stessa idea al lavoro a un livello inferiore: molti microprocessori sono in grado di eseguire le istruzioni fuori servizio, il che consente loro di utilizzare le loro varie unità di esecuzione in modo più efficiente. La chiave è che osservano le dipendenze tra le istruzioni ed evitano di riordinare dove cambierebbe il risultato.


5

Ci sono diversi argomenti per la valutazione pigra che penso siano convincenti

  1. Modularità Con la valutazione pigra è possibile suddividere il codice in parti. Ad esempio, supponiamo di avere il problema di "trovare i primi dieci reciproci di elementi in un elenco di elenchi in modo tale che i reciproci siano inferiori a 1." In qualcosa come Haskell puoi scrivere

    take 10 . filter (<1) . map (1/)
    

    ma questo non è corretto in un linguaggio rigoroso, poiché se lo dai [2,3,4,5,6,7,8,9,10,11,12,0]ti dividerai per zero. Vedi la risposta di Sacundim per il motivo per cui questo è fantastico in pratica

  2. Più cose funzionano rigorosamente (gioco di parole) più programmi terminano con una valutazione non rigorosa che con una valutazione rigorosa. Se il tuo programma termina con una strategia di valutazione "desiderosa", terminerà con una strategia "pigra", ma l'opposto non è vero. Ottieni cose come infinite strutture di dati (che in realtà sono solo piuttosto interessanti) come esempi specifici di questo fenomeno. Più programmi funzionano in lingue pigre.

  3. Ottimalità La valutazione della chiamata per necessità è asintoticamente ottimale rispetto al tempo. Sebbene le principali lingue pigre (che essenzialmente sono Haskell e Haskell) non promettono una chiamata per necessità, ci si può più o meno aspettarsi un modello di costo ottimale. Gli analizzatori di severità (e la valutazione speculativa) tengono in pratica il sovraccarico. Lo spazio è una questione più complicata.

  4. Forze La purezza usando la valutazione pigra rende il trattamento degli effetti collaterali in modo indisciplinato un dolore totale, perché, come dici tu, il programmatore perde il controllo. Questa è una buona cosa. La trasparenza referenziale semplifica notevolmente la programmazione, il rifrattore e il ragionamento sui programmi. Linguaggi rigorosi si limitano inevitabilmente alla pressione di avere pezzi impuri - qualcosa a cui Haskell e Clean hanno resistito magnificamente. Questo non vuol dire che gli effetti collaterali siano sempre malvagi, ma controllarli è così utile che questo motivo da solo è sufficiente per usare linguaggi pigri.


2

Supponiamo di avere molti calcoli costosi in offerta, ma non sai quali saranno effettivamente necessari o in quale ordine. È possibile aggiungere un protocollo Mother-May-i complicato per costringere il consumatore a capire cosa è disponibile e attivare calcoli che non sono ancora stati eseguiti. Oppure potresti semplicemente fornire un'interfaccia che agisce come se tutti i calcoli fossero stati eseguiti.

Supponi inoltre di avere un risultato infinito. L'insieme di tutti i numeri primi, ad esempio. È ovvio che non è possibile calcolare in anticipo il set, quindi qualsiasi operazione nel dominio dei numeri primi deve essere pigra.


1

con una valutazione pigra non si perde il controllo sull'esecuzione del codice, è ancora assolutamente deterministico. Però è difficile abituarsi.

la valutazione lenta è utile perché è un modo di riduzione del termine lambda che terminerà in alcuni casi, in cui la valutazione desiderosa fallirà, ma non viceversa. Ciò include 1) quando è necessario collegarsi al risultato di calcolo prima di eseguire effettivamente il calcolo, ad esempio, quando si costruisce una struttura grafica ciclica, ma si desidera farlo nello stile funzionale 2) quando si definisce una struttura di dati infinita, ma si utilizza questo feed di struttura per utilizzare solo una parte della struttura dati.

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.