Quali sono i punti di rigore di Haskell?


90

Sappiamo tutti (o dovremmo sapere) che Haskell è pigro per impostazione predefinita. Niente viene valutato fino a quando non deve essere valutato. Quindi quando deve essere valutato qualcosa? Ci sono punti in cui Haskell deve essere severo. Io chiamo questi "punti di rigore", anche se questo termine particolare non è così diffuso come pensavo. Secondo me:

La riduzione (o valutazione) in Haskell si verifica solo nei punti di rigore.

Quindi la domanda è: quali sono, precisamente , i punti di rigore di Haskell? Il mio intuito dice che main, seq/ modelli bang, pattern matching, e qualsiasi IOazione effettuata tramite mainsono i punti principali di severità, ma io non so davvero perché lo so.

(Inoltre, se non sono chiamati "punti di severità", cosa sono hanno chiamato?)

Immagino che una buona risposta includerà alcune discussioni su WHNF e così via. Immagino anche che possa toccare il lambda calcolo.


Modifica: pensieri aggiuntivi su questa domanda.

Mentre ho riflettuto su questa domanda, penso che sarebbe più chiaro aggiungere qualcosa alla definizione di un punto di rigore. I punti di rigore possono avere contesti e profondità (o rigidità) variabili . Ritornando alla mia definizione che "la riduzione di Haskell si verifica solo nei punti di rigore", aggiungiamo a questa definizione questa clausola: "un punto di rigore viene attivato solo quando il contesto circostante viene valutato o ridotto".

Quindi, fammi provare a iniziare con il tipo di risposta che voglio. mainè un punto di rigore. È appositamente designato come il principale punto di rigore del suo contesto: il programma. Quando il programma ( mainil contesto) viene valutato, viene attivato il punto di rigore principale. La profondità di Main è massima: deve essere valutata completamente. Main è solitamente composto da azioni IO, che sono anche punti di rigore, il cui contesto è main.

Ora provi: discuti seqe corrispondenza dei modelli in questi termini. Spiegare le sfumature dell'applicazione della funzione: in che modo è rigorosa? Come non lo è? Di cosa deepseq? lete casedichiarazioni? unsafePerformIO? Debug.Trace? Definizioni di primo livello? Tipi di dati rigorosi? Schemi bang? Ecc. Quanti di questi elementi possono essere descritti in termini di corrispondenza di sequenze o pattern?


10
Il tuo elenco intuitivo probabilmente non è molto ortogonale. Ho il sospetto che seqe il pattern matching siano sufficienti, con il resto definito in termini di quelli. Penso che il pattern matching assicuri la severità delle IOazioni, per esempio.
CA McCann,

Anche le primitive, come +i tipi numerici incorporati, forzano il rigore e presumo che lo stesso si applichi alle chiamate FFI pure.
Hammar

4
Sembra che qui ci siano due concetti confusi. La corrispondenza dei modelli e i modelli seq e bang sono modi in cui un'espressione può diventare rigida nelle sue sottoespressioni, ovvero, se viene valutata l'espressione superiore, lo è anche la sottoespressione. D'altra parte, le principali azioni di IO che eseguono è il modo in cui inizia la valutazione . Queste sono cose diverse ed è una sorta di errore di tipo includerle nello stesso elenco.
Chris Smith

@ChrisSmith non sto cercando di confondere questi due casi diversi; semmai chiedo ulteriori chiarimenti su come interagiscono. Il rigore avviene in qualche modo, ed entrambi i casi sono parti importanti, sebbene differenti, del "verificarsi" del rigore. (e @ monadic: ಠ_ಠ)
Dan Burton,

Se vuoi / hai bisogno di spazio per discutere aspetti di questa domanda, senza tentare una risposta completa, permettimi di suggerire di utilizzare i commenti sul mio post / r / haskell per questa domanda
Dan Burton,

Risposte:


46

Un buon punto di partenza è comprendere questo documento: A Natural Semantics for Lazy Evalution (Launchbury). Questo ti dirà quando le espressioni vengono valutate per una piccola lingua simile al Core di GHC. Quindi la domanda rimanente è come mappare Haskell completo al Core, e la maggior parte di quella traduzione è data dal rapporto Haskell stesso. In GHC chiamiamo questo processo "desugaring", perché rimuove lo zucchero sintattico.

Bene, non è tutta la storia, perché GHC include un'intera serie di ottimizzazioni tra desugaring e generazione di codice, e molte di queste trasformazioni riorganizzeranno il Core in modo che le cose vengano valutate in momenti diversi (l'analisi di rigore in particolare farà sì che le cose vengano valutate prima). Quindi per capire davvero come verrà valutato il tuo programma, devi guardare al Core prodotto da GHC.

Forse questa risposta ti sembra un po 'astratta (non ho menzionato specificamente i pattern bang o seq), ma hai chiesto qualcosa di preciso , e questo è il meglio che possiamo fare.


18
Ho sempre trovato divertente che in quello che GHC chiama "desugaring", lo zucchero sintattico rimosso includa l' effettiva sintassi del linguaggio Haskell stesso ... il che implica, potrebbe sembrare, che GHC sia in realtà un compilatore ottimizzante per GHC Linguaggio principale, che tra l'altro include anche un front-end molto elaborato per tradurre Haskell in Core. :]
CA McCann

I sistemi di tipi non funzionano con precisione però ... in particolare ma non solo per quanto riguarda la traduzione di classi tipografiche in dizionari espliciti, come ricordo. E tutte le ultime cose TF / GADT, a quanto ho capito, hanno reso ancora più ampio quel divario.
sclv


20

Probabilmente rifonderei questa domanda come: In quali circostanze Haskell valuterà un'espressione? (Forse aggiungere una "forma normale a testa debole".)

In prima approssimazione, possiamo specificarlo come segue:

  • L'esecuzione di azioni di I / O valuterà tutte le espressioni di cui hanno "bisogno". (Quindi è necessario sapere se l'azione IO viene eseguita, ad es. Il suo nome è main, o è chiamato da main E devi sapere di cosa ha bisogno l'azione.)
  • Un'espressione che viene valutata (ehi, questa è una definizione ricorsiva!) Valuterà tutte le espressioni di cui ha bisogno.

Dal tuo elenco intuitivo, le azioni principali e IO rientrano nella prima categoria e la corrispondenza di sequenze e pattern rientrano nella seconda categoria. Ma penso che la prima categoria sia più in linea con la tua idea di "punto di rigore", perché è in effetti così che facciamo in modo che la valutazione in Haskell diventi osservabile effetti per gli utenti.

Fornire tutti i dettagli in modo specifico è un compito impegnativo, poiché Haskell è un linguaggio ampio. È anche abbastanza sottile, perché Concurrent Haskell può valutare le cose in modo speculativo, anche se alla fine finiamo per non usare il risultato: questa è una terza generazione di cose che causano la valutazione. La seconda categoria è abbastanza ben studiata: si vuole guardare al rigore delle funzioni coinvolte. Anche la prima categoria può essere considerata una sorta di "rigore", anche se questo è un po 'ambiguo perché evaluate xe in seq x $ return ()realtà sono cose diverse! Puoi trattarlo correttamente se dai una sorta di semantica alla monade IO (il passaggio esplicito di un RealWorld#token funziona per casi semplici), ma non so se esiste un nome per questo tipo di analisi di rigidità stratificata in generale.


17

C ha il concetto di punti di sequenza , che sono garanzie per operazioni particolari che un operando verrà valutato prima dell'altro. Penso che sia il concetto esistente più vicino, ma il termine punto di rigore (o forse punto di forza ) essenzialmente equivalente è più in linea con il pensiero di Haskell.

In pratica Haskell non è un linguaggio puramente pigro: per esempio il pattern matching è solitamente rigoroso (quindi provare un pattern match costringe la valutazione ad avvenire almeno abbastanza lontano da accettare o rifiutare la corrispondenza.

...

I programmatori possono anche utilizzare la seqprimitiva per forzare la valutazione di un'espressione indipendentemente dal fatto che il risultato venga mai utilizzato.

$!è definito in termini di seq.

- Pigro vs non rigoroso .

Quindi il tuo pensiero su !/ $!ed seqè essenzialmente giusto, ma il pattern matching è soggetto a regole più sottili. Puoi sempre usare ~per forzare la corrispondenza del modello pigro, ovviamente. Un punto interessante da quello stesso articolo:

L'analizzatore di severità cerca anche i casi in cui le sottoespressioni sono sempre richieste dall'espressione esterna e le converte in una valutazione avida. Può farlo perché la semantica (in termini di "fondo") non cambia.

Continuiamo nella tana del coniglio e guardiamo i documenti per le ottimizzazioni eseguite da GHC:

L'analisi di rigore è un processo mediante il quale GHC tenta di determinare, in fase di compilazione, quali dati saranno sicuramente "sempre necessari". GHC può quindi creare codice per calcolare tali dati, invece del normale processo (overhead più elevato) per memorizzare il calcolo ed eseguirlo in un secondo momento.

- Ottimizzazioni GHC: analisi di rigore .

In altre parole, il codice rigoroso può essere generato ovunque come un'ottimizzazione, perché la creazione di thunk è inutilmente costosa quando i dati saranno sempre necessari (e / o possono essere utilizzati solo una volta).

… Non è più possibile effettuare valutazioni sul valore; si dice che sia in forma normale . Se siamo in uno qualsiasi dei passaggi intermedi in modo da aver eseguito almeno una valutazione su un valore, è in forma normale della testa debole (WHNF). (C'è anche una 'forma normale della testa', ma non è usata in Haskell.) Valutare completamente qualcosa in WHNF lo riduce a qualcosa in forma normale ...

- Wikibooks Haskell: pigrizia

(Un termine è in forma normale di testa se non c'è beta-redex nella posizione di testa 1. Un redex è un redex di testa se è preceduto solo da astrattori lambda di non-redex 2. ) Quindi, quando inizi a forzare un thunk, stai lavorando in WHNF; quando non ci sono più thunk da forzare, sei in forma normale. Un altro punto interessante:

... se ad un certo punto avessimo bisogno, diciamo, di stampare z per l'utente, avremmo bisogno di valutarlo completamente ...

Il che implica, naturalmente, che, in effetti, qualsiasi IOazione eseguita da main fa la valutazione forza, che dovrebbe essere ovvio se si considera che i programmi di Haskell fare, infatti, fanno le cose. Tutto ciò che deve passare attraverso la sequenza definita in maindeve essere nella forma normale ed è quindi soggetto a valutazione rigorosa.

CA McCann ha capito bene nei commenti, però: l'unica cosa di speciale mainè che mainè definita speciale; il pattern matching sul costruttore è sufficiente per garantire la sequenza imposta dalla IOmonade. Solo sotto questo aspetto seqe il pattern-matching sono fondamentali.


4
In realtà la citazione "se ad un certo punto avessimo bisogno, diciamo, di stampare z per l'utente, avremmo bisogno di valutarla completamente" non è del tutto corretta. È rigoroso come l' Showistanza del valore stampato.
nominolo

10

Haskell è AFAIK non un puro linguaggio pigro, ma piuttosto un linguaggio non rigoroso. Ciò significa che non valuta necessariamente i termini all'ultimo momento possibile.

Una buona fonte per il modello di "pigrizia" di haskell può essere trovata qui: http://en.wikibooks.org/wiki/Haskell/Laziness

Fondamentalmente, è importante comprendere la differenza tra un thunk e il modulo normale di intestazione debole WHNF.

La mia comprensione è che haskell esegue i calcoli all'indietro rispetto ai linguaggi imperativi. Ciò significa che in assenza di schemi "seq" e bang, alla fine sarà un qualche tipo di effetto collaterale che forza la valutazione di un thunk, che a sua volta può causare valutazioni precedenti (vera pigrizia).

Poiché ciò porterebbe a un'orribile perdita di spazio, il compilatore quindi capisce come e quando valutare i thunk in anticipo per risparmiare spazio. Il programmatore può quindi supportare questo processo fornendo annotazioni di rigore (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) per ridurre ulteriormente l'utilizzo dello spazio sotto forma di thunk nidificati.

Non sono un esperto della semantica operativa di haskell, quindi lascerò il collegamento come risorsa.

Altre risorse:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation


6

Pigro non significa non fare nulla. Ogni volta che il pattern del tuo programma corrisponde a caseun'espressione, valuta qualcosa, quanto basta comunque. Altrimenti non riesce a capire quale RHS usare. Non vedi alcuna espressione case nel tuo codice? Non preoccuparti, il compilatore sta traducendo il tuo codice in una forma ridotta di Haskell dove è difficile evitare di usarli.

Per un principiante, una regola pratica di base è che letè pigro, caseè meno pigro.


2
Si noti che sebbene caseimponga sempre la valutazione in GHC Core, non lo fa nel normale Haskell. Ad esempio, prova case undefined of _ -> 42.
Hammar

2
casein GHC Core valuta il suo argomento a WHNF, mentre casein Haskell valuta il suo argomento quanto necessario per selezionare il ramo appropriato. Nell'esempio di Hammar, questo non è affatto, ma in case 1:undefined of x:y:z -> 42, valuta più in profondità di WHNF.
Max

E inoltre case something of (y,x) -> (x,y)non ha bisogno di valutare somethingaffatto. Questo è vero per tutti i tipi di prodotto.
Ingo

@ Ingo - non è corretto. somethingdovrebbe essere valutato su WHNF per raggiungere il costruttore di tupla.
John L,

John - Perché? Sappiamo che deve essere una tupla, quindi qual è il punto di valutarla? È sufficiente se x e y sono vincolati al codice che valuta la tupla ed estrae lo slot appropriato, se mai fossero necessari.
Ingo

4

Questa non è una risposta completa che mira al karma, ma solo un pezzo del puzzle - nella misura in cui si tratta di semantica, tieni presente che ci sono più strategie di valutazione che forniscono la stessa semantica. Un buon esempio qui - e il progetto parla anche di come pensiamo tipicamente alla semantica Haskell - è stato il progetto Eager Haskell, che ha modificato radicalmente le strategie di valutazione mantenendo la stessa semantica: http://csg.csail.mit.edu/ pubs / haskell.html


2

Il compilatore Glasgow Haskell traduce il codice in un linguaggio simile a Lambda calcolo chiamato core . In questo linguaggio, qualcosa verrà valutato, ogni volta che lo caseabbini a un pattern con un'istruzione. Pertanto, se viene chiamata una funzione, verrà valutato il costruttore più esterno e solo esso (se non ci sono campi forzati). Qualsiasi altra cosa è inscatolata in un thunk. (I thunk vengono introdotti dalle letassociazioni).

Ovviamente questo non è esattamente ciò che accade nella lingua reale. Il compilatore converte Haskell in Core in un modo molto sofisticato, rendendo il maggior numero di cose possibilmente pigro e tutto ciò che è sempre necessario pigro. Inoltre, ci sono valori unboxed e tuple sempre rigorose.

Se provi a valutare manualmente una funzione, puoi sostanzialmente pensare:

  • Prova a valutare il costruttore più esterno del rendimento.
  • Se è necessario qualcos'altro per ottenere il risultato (ma solo se è davvero necessario) verrà valutato anche. L'ordine non ha importanza.
  • In caso di IO devi valutare i risultati di tutte le affermazioni dalla prima all'ultima in quella. Questo è un po 'più complicato, poiché la monade IO fa alcuni trucchi per forzare la valutazione in un ordine specifico.
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.