Cosa significa il punto esclamativo in una dichiarazione di Haskell?


262

Mi sono imbattuto nella seguente definizione mentre provo ad imparare Haskell usando un vero progetto per guidarlo. Non capisco cosa significhi il punto esclamativo di fronte a ogni argomento e i miei libri non sembrano menzionarlo.

data MidiMessage = MidiMessage !Int !MidiMessage

13
Ho il sospetto che questa potrebbe essere una domanda molto comune; Ricordo chiaramente di essermi interrogato sulla stessa identica cosa, molto tempo fa.
cjs,

Risposte:


314

È una dichiarazione di rigore. Fondamentalmente, significa che deve essere valutato in quello che viene chiamato "modulo normale testa debole" quando viene creato il valore della struttura dei dati. Diamo un'occhiata a un esempio, in modo che possiamo vedere esattamente cosa significa:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

La funzione fsopra, quando valutata, restituirà un "thunk": cioè il codice da eseguire per capirne il valore. A quel punto, un Foo non esiste ancora, solo il codice.

Ma a un certo punto qualcuno potrebbe provare a guardarci dentro, probabilmente attraverso una combinazione di schemi:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Questo eseguirà abbastanza codice per fare ciò di cui ha bisogno, e non di più. Quindi creerà un Foo con quattro parametri (perché non puoi guardarci dentro senza che esistano). Il primo, dal momento che lo stiamo testando, dobbiamo valutare fino in fondo 4, dove ci rendiamo conto che non corrisponde.

Il secondo non ha bisogno di essere valutato, perché non lo stiamo testando. Così, invece di 6essere memorizzato in quella posizione di memoria, ci limiteremo a memorizzare il codice per un eventuale successiva valutazione, (3+3). Ciò si trasformerà in un 6 solo se qualcuno lo guarda.

Il terzo parametro, tuttavia, ha un !davanti, quindi viene valutato rigorosamente: (4+4)viene eseguito e 8viene archiviato in quella posizione di memoria.

Anche il quarto parametro viene valutato rigorosamente. Ma qui è dove diventa un po 'complicato: stiamo valutando non completamente, ma solo a una forma della testa normale debole. Ciò significa che capiamo se è Nothingo Justqualcosa del genere e lo memorizziamo, ma non andiamo oltre. Ciò significa che non immagazziniamo Just 10ma in realtà Just (5+5), lasciando il thunk all'interno non valutato. Questo è importante da sapere, anche se penso che tutte le implicazioni di ciò vadano piuttosto oltre lo scopo di questa domanda.

È possibile annotare gli argomenti della funzione allo stesso modo, se si abilita l' BangPatternsestensione della lingua:

f x !y = x*y

f (1+1) (2+2)restituirà il thunk (1+1)*4.


16
Questo è molto utile Non posso fare a meno di chiedermi tuttavia se la terminologia di Haskell stia rendendo le cose più complicate da comprendere per le persone con termini come "forma della testa normale debole", "severo" e così via. Se ti capisco correttamente, sembra che! operatore significa semplicemente memorizzare il valore valutato di un'espressione piuttosto che archiviare un blocco anonimo per valutarlo in un secondo momento. È un'interpretazione ragionevole o c'è qualcosa in più?
David,

71
@David La domanda è quanto lontano valutare il valore. Forma normale debole: significa valutarla fino a raggiungere il costruttore più esterno. Forma normale significa valutare l'intero valore fino a quando non vengono lasciati componenti non valutati. Poiché Haskell consente tutti i livelli di profondità di valutazione, ha una terminologia ricca per descriverlo. Non si tende a trovare queste distinzioni in lingue che supportano solo la semantica di chiamata per valore.
Don Stewart,

8
@ David: ho scritto una spiegazione più approfondita qui: Haskell: Che cos'è la forma normale testa debole? . Anche se non menziono schemi di botto o annotazioni di rigore, sono equivalenti all'utilizzo seq.
Hammar

1
Giusto per essere sicuro di aver capito: quella pigrizia riguarda solo argomenti sconosciuti in fase di compilazione? Vale a dire se il codice sorgente avesse davvero un'istruzione (2 + 2) , sarebbe comunque ottimizzato su 4 invece di avere un codice per aggiungere i numeri noti?
Ciao Angelo

1
Ciao Angel, dipende davvero da quanto il compilatore vuole ottimizzare. Sebbene usiamo la frangetta per dire che vorremmo forzare alcune cose a indebolire la normale forma della testa, non abbiamo (e non abbiamo né bisogno né desiderio) un modo per dire "per favore assicurati di non valutare ancora questo thunk un passo avanti." Da un punto di vista semantico, dato un thunk "n = 2 + 2", non si può nemmeno dire se un particolare riferimento a n è stato valutato ora o è stato precedentemente valutato.
cjs,

92

Un modo semplice per vedere la differenza tra argomenti costruttivi rigidi e non rigidi è come si comportano quando non sono definiti. Dato

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Poiché l'argomento non rigoroso non viene valutato da second, il passaggio undefinednon causa un problema:

> second (Foo undefined 1)
1

Ma l'argomento rigoroso non può essere undefined, anche se non utilizziamo il valore:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined

2
Questo è meglio della risposta di Curt Sampson perché in realtà spiega gli effetti osservabili dall'utente che il !simbolo ha, piuttosto che approfondire i dettagli dell'implementazione interna.
David Grayson,

26

Credo che sia un'annotazione rigorosa.

Haskell è un linguaggio funzionale puro e pigro , ma a volte il sovraccarico della pigrizia può essere troppo o dispendioso. Quindi, per far fronte a questo, puoi chiedere al compilatore di valutare completamente gli argomenti in una funzione invece di analizzare i thunk intorno.

Ci sono ulteriori informazioni in questa pagina: Prestazioni / Rigidità .


Hmm, l'esempio su quella pagina a cui hai fatto riferimento sembra parlare dell'uso di! quando si richiama una funzione. Ma questo sembra diverso dal metterlo in una dichiarazione di tipo. Cosa mi sto perdendo?
David,

La creazione di un'istanza di un tipo è anche un'espressione. Puoi pensare ai costruttori di tipi come funzioni che restituiscono nuove istanze del tipo specificato, dati gli argomenti forniti.
Chris Vest,

4
In effetti, a tutti gli effetti, i costruttori di tipi sono funzioni; puoi applicarli parzialmente, passarli ad altre funzioni (ad esempio, map Just [1,2,3]per ottenere [Solo 1, Solo 2, Solo 3]) e così via. Trovo utile pensare alla capacità di modellare la corrispondenza con loro e una struttura completamente indipendente.
cjs,
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.