Domanda

Mi chiedo da tempo perché la valutazione pigra sia utile. Devo ancora che qualcuno mi spieghi in modo sensato; principalmente finisce per ridursi a "fidati di me".

Nota: non intendo memoization.

È stato utile?

Soluzione

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, in realtà può essere utilizzato solo un sottoinsieme. In un linguaggio come C, tutti e tre i valori verrebbero comunque calcolati; ma in Haskell vengono calcolati solo i valori necessari.

Permette anche cose interessanti come liste infinite. Non posso avere un elenco infinito in un linguaggio 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.

Altri suggerimenti

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 dell'elenco, possiamo definire

minimum ls = head (quickSort ls)

Quale prima ordina l'elenco e quindi accetta il primo elemento dell'elenco. Tuttavia, a causa della valutazione pigra, viene calcolata solo la testa. Ad esempio, se prendiamo il minimo dell'elenco [2, 1, 3,] quickSort filtrerà prima tutti gli elementi che sono più piccoli di due. Quindi esegue QuickSort su quello (restituendo l'elenco singleton [1]) che è già abbastanza. 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 programmi molto grandi.

C'è tuttavia un aspetto negativo in tutto ciò: diventa più difficile prevedere la velocità di runtime e l'utilizzo della memoria del programma. Questo non significa che i programmi pigri siano più lenti o occupino più memoria, ma è bene saperlo.

Trovo la valutazione pigra 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 ti consentono di ragionare sulle definizioni delle funzioni usando il ragionamento equazionale.

foo x = x + 3

Sfortunatamente in un ambiente non pigro, non vengono restituite più istruzioni che in un ambiente pigro, quindi questo è meno utile in lingue come ML. Ma in un linguaggio pigro puoi tranquillamente ragionare sull'uguaglianza.

In secondo luogo, molte cose come la "limitazione del valore" in ML non sono necessarie in linguaggi pigri come Haskell. Ciò porta a un grande decluttering della sintassi. ML come le lingue devono usare parole chiave come var o divertimento. In Haskell queste cose crollano in una sola idea.

In terzo luogo, la pigrizia ti consente di scrivere un codice molto funzionale che può essere compreso in 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ò ti consente di lavorare "dall'alto verso il basso" attraverso la comprensione del corpo di una funzione. Linguaggi di tipo ML ti costringono a usare un let che viene valutato rigorosamente. Di conseguenza, non osi "alzare" la clausola let al corpo principale della funzione, perché se è costosa (o ha effetti collaterali) non vuoi che venga sempre valutata. Haskell può "trasferire" i dettagli nella clausola where esplicitamente perché sa che i contenuti di quella clausola saranno valutati solo se necessario.

In pratica, tendiamo a usare le guardie e collassiamo ulteriormente per:

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

In quarto luogo, la pigrizia a volte offre un'espressione molto più elegante di alcuni algoritmi. Un 'ordinamento rapido' pigro in Haskell è un one-liner e ha il vantaggio che se si guardano solo i primi articoli, si pagano solo costi proporzionali al costo della selezione di quegli stessi articoli. Nulla ti impedisce di farlo rigorosamente, 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 "se ... poi ... altro ..." come un costrutto in un linguaggio rigoroso. Se si tenta di definire una funzione come:

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

in un linguaggio rigoroso, entrambi i rami verrebbero valutati indipendentemente dal valore della condizione. Peggiora quando si considerano i loop. Tutte le soluzioni rigorose richiedono che il linguaggio fornisca una sorta di preventivo o esplicita costruzione lambda.

Infine, in quella stessa ottica, alcuni dei migliori meccanismi per gestire gli effetti collaterali nel sistema dei tipi, come le monadi, possono davvero essere espressi efficacemente solo in un ambiente pigro. Ciò può essere dimostrato confrontando la complessità dei flussi di lavoro di F # con le monadi Haskell. (Puoi definire una monade in un linguaggio rigoroso, ma sfortunatamente spesso fallisci una o due leggi della monade a causa della mancanza di pigrizia e flussi di lavoro al confronto raccogli una tonnellata di bagagli rigorosi.)

C'è una differenza tra la normale valutazione dell'ordine e una valutazione lazy (come in Haskell).

square x = x * x

Valutazione dell'espressione seguente ...

square (square (square 2))

... con valutazione entusiasta:

> 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 lazy guarda l'albero della sintassi e fa trasformazioni dell'albero ...

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

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

           ||
           \/

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

... mentre la normale valutazione dell'ordine fa solo espansioni testuali.

Ecco perché noi, quando utilizziamo la valutazione pigra, diventiamo più potenti (la valutazione termina più spesso di altre strategie) mentre le prestazioni sono equivalenti alla valutazione desiderosa (almeno nella notazione O).

Valutazione pigra correlata 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 oggetti inutilizzabili. LE ti consente di fingere di disporre di risorse computazionali illimitate: puoi fare tutti i calcoli di cui hai bisogno. Il runtime non eseguirà calcoli non necessari (per un determinato caso).

Qual è il vantaggio pratico di questi "fingendo"? Modelli? Libera lo sviluppatore (in una certa misura) dalla gestione delle risorse e rimuove un po 'di codice boilerplate dalle tue fonti. Ma la cosa più importante è che puoi riutilizzare efficacemente la tua soluzione in una più ampia serie di contesti.

Immagina di avere un elenco di numeri S e un numero N. Devi trovare il numero più vicino al numero N numero M dall'elenco S. Puoi avere due contesti: singolo N e qualche elenco L di N (ei per ciascuno N in L cerchi la M più vicina in S). Se si utilizza la valutazione lazy, è possibile ordinare S e applicare la ricerca binaria per trovare la M più vicina a N. Per un buon ordinamento lazy saranno necessari passaggi O (dimensione (S)) per N e O singoli (ln (dimensione (S)) * (size (S) + size (L))) per equamente distribuito L. Se non si dispone di una valutazione pigra per raggiungere l'efficienza ottimale, è necessario implementare l'algoritmo per ciascun 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 straordinarie con una valutazione pigra. Leggere il loro lavoro ti aiuterà ad apprezzarlo. A buoni punti di partenza sono il magnifico sudoku di Bird e il documento di Hughes su Perché è importante la programmazione funzionale .

Prendi in considerazione un programma tic-tac-toe. Ha quattro funzioni:

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

Questo crea una bella chiara separazione delle preoccupazioni. In particolare, la funzione di generazione delle mosse e le funzioni di valutazione del tabellone sono le uniche che devono comprendere le regole del gioco: le funzioni di albero delle mosse e minimax sono completamente riutilizzabili.

Ora proviamo a implementare gli scacchi invece di tic-tac-toe. In un "desideroso" (cioè convenzionale) linguaggio non funzionerà perché l'albero di spostamento non si adatta alla memoria. Quindi ora le funzioni di valutazione del board e di generazione dei movimenti devono essere mescolate con l'albero dei movimenti e la logica minimax perché la logica minimax deve essere usata per decidere quali mosse generare. La nostra bella struttura modulare pulita scompare.

Tuttavia, in un linguaggio pigro, gli elementi dell'albero delle mosse vengono generati solo in risposta alle richieste della funzione minimax: non è necessario generare l'intero albero delle mosse prima di lasciar perdere minimax sull'elemento superiore. Quindi la nostra struttura modulare pulita funziona ancora in un vero gioco.

Ecco altri due punti che non credo siano ancora stati sollevati nella discussione.

  1. La pigrizia è un meccanismo di sincronizzazione in un ambiente concorrente. È un modo semplice e leggero per creare un riferimento ad alcuni calcoli e condividere i suoi risultati tra molti thread. Se più thread tentano di accedere a un valore non valutato, solo uno di essi lo eseguirà e gli altri lo bloccheranno di conseguenza, ricevendo il valore non appena sarà disponibile.

  2. La pigrizia è fondamentale per ammortizzare le strutture di dati in un ambiente puro. Questo è descritto da Okasaki in Strutture di dati puramente funzionali in dettaglio, ma l'idea di base è che la valutazione pigra è una forma controllata di mutazione fondamentale per permetterci di implementare determinati tipi di strutture di dati in modo efficiente. Mentre parliamo spesso di pigrizia che ci obbliga a indossare la peli della purezza, si applica anche l'altro modo: sono un paio di caratteristiche del linguaggio sinergico.

Quando si accende il computer e Windows si astiene dall'aprire ogni singola directory sul disco rigido in Esplora risorse e si astiene dall'avvio di ogni singolo programma installato sul computer, fino a quando si indica che è necessaria una determinata directory o un determinato programma è necessario, ovvero "pigro" valutazione.

" Pigro " la valutazione sta eseguendo operazioni quando e quando sono necessarie. È utile quando si tratta di una caratteristica di un linguaggio di programmazione o di una libreria perché è generalmente più difficile implementare una valutazione pigra da soli piuttosto che precalcolare tutto in anticipo.

  1. Può aumentare l'efficienza. Questo è 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 usare una quantità enorme di RAM.)

  2. Ti permette di definire costrutti di controllo di flusso in un normale codice a livello di utente, piuttosto che essere codificato nel linguaggio. (Ad esempio, Java ha i cicli per ; Haskell ha una funzione for . Java ha la gestione delle eccezioni; Haskell ha vari tipi di monade delle eccezioni. C # ha goto ; Haskell ha la monade di continuazione ...)

  3. Ti consente di disaccoppiare l'algoritmo per generare i dati dall'algoritmo per decidere quanti generare. È possibile scrivere una funzione che genera un elenco di risultati teoricamente infinito e un'altra funzione che elabora tanto di questo elenco quanto decide di cui ha bisogno. Più precisamente, puoi avere cinque funzioni generatore e cinque funzioni 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. Ti costringe più o meno a progettare un linguaggio funzionale puro . È sempre allettante prendere scorciatoie, ma in un linguaggio pigro, la minima impurità rende imprevedibile il tuo codice selvaggiamente , che milita fortemente contro le scorciatoie.

Considera questo:

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

Il metodo doSomething () verrà eseguito solo se conditionOne è true e conditionTwo è true. Nel caso in cui conditionOne sia falso, perché è necessario calcolare il risultato della condizione Two? La valutazione della condizione Due sarà una perdita di tempo in questo caso, soprattutto se la tua condizione è il risultato di un processo di metodo.

Questo è un esempio dell'interesse per la valutazione pigro ...

Un enorme vantaggio della pigrizia è la capacità di scrivere strutture di dati immutabili con limiti ammortizzati ragionevoli. Un semplice esempio è uno stack immutabile (usando 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 pile xey richiede il 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 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 su ogni .Force () successivo.

Con la versione lazy, gli allegati sono un'operazione O (1): restituisce 1 nodo e sospende la ricostruzione effettiva dell'elenco. Quando ottieni la testa di questo elenco, valuterà il contenuto del nodo, costringendolo a restituire la testa e creare una sospensione con gli elementi rimanenti, quindi prendere la testa della lista è un'operazione O (1).

Quindi, la nostra lista pigra è in uno stato costante di ricostruzione, non paghi il costo per ricostruire questa lista finché non attraversi tutti i suoi elementi. Usando la pigrizia, questo elenco supporta il consing e l'aggiunta di O (1). È interessante notare che, poiché non valutiamo i nodi fino al loro accesso, è del tutto possibile costruire un elenco con elementi potenzialmente infiniti.

La struttura di dati sopra non richiede che i nodi vengano ricalcolati su ogni attraversamento, quindi sono nettamente diversi da IEnumerables vaniglia in .NET.

La valutazione pigra è molto utile con le strutture di dati. È possibile definire un array o un vettore specificando induttivamente solo alcuni punti della struttura ed esprimendo tutti gli altri in termini di intero array. Ciò consente di generare strutture di dati in modo molto conciso e con prestazioni di runtime elevate.

Per vederlo in azione, puoi dare un'occhiata alla mia libreria di reti neurali chiamata istinto . Fa un uso pesante della valutazione pigra per eleganza e prestazioni elevate. Ad esempio, mi libero totalmente dal calcolo dell'attivazione tradizionalmente imperativo. Una semplice espressione pigra fa tutto per me.

Questo è usato ad esempio nella funzione di attivazione e anche nell'algoritmo di apprendimento di backpropagation (posso solo pubblicare due link, quindi dovrai cercare la funzione learnPat nel Modulo AI.Instinct.Train.Delta ). Tradizionalmente entrambi richiedono algoritmi iterativi molto più complicati.

Questo frammento mostra la differenza tra valutazione pigra e non pigra. Naturalmente questa funzione fibonacci potrebbe essere ottimizzata e utilizzare una valutazione pigra invece della ricorsione, ma ciò rovinerebbe l'esempio.

Supponiamo che MAGGIO dobbiamo usare i primi 20 numeri per qualcosa, con una valutazione non pigra tutti i 20 numeri devono essere generati in anticipo, ma, con una valutazione pigra, saranno generati secondo necessità solo. In questo modo pagherai solo il prezzo di calcolo quando necessario.

Output di esempio

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)

Altre persone hanno già dato tutte le ragioni principali, ma penso che un esercizio utile per aiutare a capire perché la pigrizia sia importante è provare a scrivere una punto fisso in un linguaggio rigoroso.

In Haskell, una funzione a virgola fissa è semplicissima:

fix f = f (fix f)

questo si espande in

f (f (f ....

ma poiché Haskell è pigro, quella catena infinita di calcolo non è un problema; la valutazione è fatta "dall'esterno all'interno", e tutto funziona meravigliosamente:

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

È importante sottolineare che non importa che fix sia pigro, ma che f sia pigro. Una volta che ti è già stato assegnato un f rigoroso, puoi o lanciare le mani in aria e arrenderti, oppure eta espanderlo e ingombrare le cose. (È molto simile a ciò che Noah stava dicendo riguardo al fatto che si tratta della libreria che è rigorosa / pigra, non la lingua).

Ora immagina di scrivere la stessa funzione nel rigoroso Scala:

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 si ottiene un overflow dello stack. Se vuoi che funzioni, devi mettere in discussione l'argomento f :

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 attualmente alle cose, ma trovo utile pensare alla valutazione pigra come un problema di biblioteca piuttosto che una funzione linguistica.

Voglio dire che in linguaggi rigorosi, posso implementare una valutazione pigra costruendo alcune strutture di dati, e in 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 influenza semplicemente ciò che ottieni di default.

Una volta che lo pensi in questo modo, pensa a tutti i luoghi in cui scrivi una struttura di dati che puoi utilizzare in seguito per generare dati (senza guardarli troppo prima di allora), e vedrai un sacco di utilizza per la valutazione pigra.

Lo sfruttamento più utile della valutazione pigra che ho usato era una funzione che chiamava una serie di sotto-funzioni in un ordine particolare. Se una 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;
}

o, la soluzione più elegante:

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

Quando inizi a usarlo, vedrai l'opportunità di 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 infinite di dati multidimensionali.

Mentre schema, python, ecc. consentono strutture dati infinite monodimensionali con flussi, è possibile attraversare solo una dimensione.

La pigrizia è utile per stesso problema di frange , ma vale la pena notare la connessione coroutines menzionata in quel link.

La valutazione pigra è il ragionamento equazionale del povero (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. prendi 10 $ [1..10000000000] . Che non ci dispiace essere ridotto a una somma di 10 numeri, invece di un solo calcolo numerico diretto e semplice. Senza la valutazione pigra, ovviamente, ciò creerebbe un gigantesco elenco in memoria solo per usare i suoi primi 10 elementi. Sarebbe certamente molto lento e potrebbe causare un errore di memoria insufficiente.

Esempio in cui non è eccezionale come vorremmo: sum. prendere 1000000. rilasciare 500 $ ciclo [1..20] . Che sommerà effettivamente i 1 000 000 di numeri, anche se in un ciclo anziché in un elenco; tuttavia dovrebbe essere ridotto a un solo calcolo numerico diretto, con pochi condizionali e poche formule. Quale sarebbe molto meglio che riassumendo i 1 000 000 di numeri. Anche se in un ciclo e non in un elenco (cioè dopo l'ottimizzazione della deforestazione).


Un'altra cosa è, rende possibile codificare in coda ricorsione modulo contro stile, e funziona solo .

cf. risposta correlata .

Se per " valutazione lenta " intendi come nei booleani in combound, come in

   if (ConditionA && ConditionB) ... 

allora la risposta è semplicemente che minore è il numero di cicli della CPU consumati dal programma, maggiore sarà la sua velocità di esecuzione ... e se una parte delle istruzioni di elaborazione non avrà alcun impatto sul risultato del programma, allora non sarà necessario (e quindi una perdita di tempo) per eseguirli comunque ...

se otoh, intendi quello 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 set 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 istanza di un Dipendente più velocemente, e tuttavia quando è necessario il supervisore, la prima chiamata alla proprietà supervisore attiverà la chiamata al database e i dati verranno recuperati e disponibili ...

Estratto da Funzioni di ordine superiore

  

Troviamo il numero più grande sotto 100.000 che è divisibile per 3829.   Per fare ciò, filtreremo solo una serie di possibilità in cui sappiamo   la soluzione sta.

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

In primo luogo facciamo un elenco di tutti i numeri inferiori a 100.000, in ordine decrescente.   Quindi lo filtriamo in base al nostro predicato e perché i numeri sono ordinati   in modo decrescente, il numero più grande che soddisfa il nostro   predicato è il primo elemento dell'elenco filtrato. Non abbiamo nemmeno   è necessario utilizzare un elenco finito per il nostro set di partenza. Questa è pigrizia   di nuovo azione. Perché finiamo solo per usare la testa del filtrato   elenco, non importa se l'elenco filtrato è finito o infinito.   La valutazione si interrompe quando viene trovata la prima soluzione adeguata.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top