Quali sono le regole su una funzione a -> () in fase di valutazione in Haskell?


12

Proprio come dice il titolo: quali garanzie ci sono per una unità di ritorno della funzione Haskell da valutare? Si potrebbe pensare che non sia necessario eseguire alcun tipo di valutazione in tal caso, il compilatore potrebbe sostituire tutte queste chiamate con un ()valore immediato a meno che non siano presenti richieste esplicite di rigore, nel qual caso il codice potrebbe dover decidere se dovrebbe ritorno ()o fondo.
Ho sperimentato questo in GHCi, e sembra che succede il contrario, cioè una tale funzione sembra essere valutata. Un esempio molto primitivo sarebbe

f :: a -> ()
f _ = undefined

La valutazione f 1genera un errore dovuto alla presenza di undefined, quindi alcune valutazioni avvengono sicuramente. Tuttavia, non è chiaro quanto sia approfondita la valutazione; a volte sembra andare tanto in profondità quanto è necessario per valutare tutte le chiamate alle funzioni che ritornano (). Esempio:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Questo codice viene ripetuto indefinitamente se presentato g (let x = 1:x in x). Ma allora

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

può essere utilizzato per mostrare che h (f 1)restituisce (), quindi in questo caso non vengono valutate tutte le sottoespressioni valutate in unità. Qual è la regola generale qui?

ETA: ovviamente conosco la pigrizia. Sto chiedendo cosa impedisce agli autori di compilatori di rendere questo caso particolare ancora più pigro del solito.

ETA2: sintesi degli esempi: GHC sembra trattare ()esattamente come qualsiasi altro tipo, cioè come se ci fosse una domanda su quale valore regolare che abita nel tipo dovrebbe essere restituito da una funzione. Il fatto che esista un solo valore non sembra (ab) utilizzato dagli algoritmi di ottimizzazione.

ETA3: quando dico Haskell, intendo Haskell come definito dal Rapporto, non Haskell-H-in-GHC. Sembra un'ipotesi non condivisa così ampiamente come immaginavo (che era "dal 100% dei lettori"), o probabilmente sarei stato in grado di formulare una domanda più chiara. Ciò nonostante, mi dispiace cambiare il titolo della domanda, poiché inizialmente chiedeva quali garanzie esistano per una tale funzione chiamata.

ETA4: sembrerebbe che questa domanda abbia fatto il suo corso e la considero senza risposta. (Stavo cercando una funzione di "domanda stretta" ma ho trovato solo "rispondi alla tua domanda" e poiché non è possibile rispondere, non ho seguito quella strada. Nessuno ha sollevato nulla dal Rapporto che lo avrebbe deciso in entrambi i modi , che sono tentato di interpretare come una risposta forte ma non definita "nessuna garanzia per la lingua in quanto tale". Tutto quello che sappiamo è che l'attuale implementazione GHC non salterà la valutazione di tale funzione.

Ho riscontrato il problema reale durante il porting di un'app OCaml su Haskell. L'app originale aveva una struttura reciprocamente ricorsiva di molti tipi e il codice dichiarava un numero di funzioni chiamate assert_structureN_is_correctN in 1..6 o 7, ognuna delle quali restituiva unità se la struttura era effettivamente corretta e generava un'eccezione se non lo era . Inoltre, queste funzioni si chiamavano a vicenda mentre decomposivano le condizioni di correttezza. In Haskell questo è meglio gestito usando la Either Stringmonade, quindi l'ho trascritta in quel modo, ma la questione come questione teorica è rimasta. Grazie per tutti gli input e le risposte.


1
Questa è pigrizia sul lavoro. A meno che non sia richiesto il risultato di una funzione (ad es. Mediante la corrispondenza del modello rispetto a un costruttore), il corpo della funzione non viene valutato. Per osservare la differenza, prova a confrontare h1::()->() ; h1 () = ()e h2::()->() ; h2 _ = (). Esegui entrambi h1 (f 1)e h2 (f 1)vedi che solo il primo richiede (f 1).
Chi,

1
"La pigrizia sembrerebbe imporre che venga sostituito da () senza che si verifichi alcun tipo di valutazione." Cosa significa? f 1viene "sostituito" undefinedin tutti i casi.
oisdk,

3
Una funzione ... -> ()può 1) terminare e restituire (), 2) terminare con un errore di eccezione / runtime e non riuscire a restituire nulla, oppure 3) divergere (ricorsione infinita). GHC non ottimizza il codice presupponendo che solo 1) possa accadere: se f 1richiesto, non salta la sua valutazione e restituisce (). La semantica di Haskell è di valutarla e vedere cosa succede tra 1,2,3.
Chi,

2
Non c'è davvero nulla di speciale in ()(o il tipo o il valore) in questa domanda. Tutte le stesse osservazioni accadono se si sostituisce () :: (), diciamo, 0 :: Intovunque. Queste sono solo vecchie e noiose conseguenze della valutazione pigra.
Daniel Wagner,

2
no, "evitare" ecc. non è la semantica di Haskell. e ci sono due possibili valori di ()tipo ()e undefined.
Will Ness,

Risposte:


10

Sembra che tu provenga dal presupposto che il tipo ()abbia un solo valore possibile ()e quindi ti aspetti che qualsiasi chiamata di funzione che restituisca un valore di tipo ()debba automaticamente assumere che produca effettivamente il valore ().

Haskell non funziona così. Ogni tipo ha un valore in più in Haskell, vale a dire nessun valore, errore, o cosiddetto "bottom", codificato da undefined. Quindi una valutazione sta effettivamente avvenendo:

main = print (f 1)

è equivalente al linguaggio di base

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

o anche (*)

main = _Case (f 1) _Of x -> putStr "()"

e Core _Casesta forzando :

"La valutazione di una %case[espressione] impone la valutazione dell'espressione da testare (la" scrutinee "). Il valore della scrutinee è legato alla variabile che segue la %ofparola chiave, ...".

Il valore è costretto a indebolire la forma normale della testa. Questo fa parte della definizione della lingua.

Haskell non è un linguaggio di programmazione dichiarativo .


(*) print x = putStr (show x) e show () = "()", quindi, la showchiamata può essere compilata del tutto.

Il valore è infatti noto in anticipo come ()e anche il valore di show ()è noto in anticipo come "()". Ancora la semantica Haskell accettati esigono che il valore di (f 1)è costretto a debole forma normale testa prima di procedere con la stampa che noto nella stringa anticipo, "()".


modifica: considera concat (repeat []). Dovrebbe essere []o dovrebbe essere un ciclo infinito?

La risposta di un "linguaggio dichiarativo" è molto probabilmente []. La risposta di Haskell è, il ciclo infinito .

La pigrizia è "la programmazione dichiarativa del povero", ma non è ancora la cosa reale .

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()e solo hè forzato, no f; e per produrre la sua risposta hnon deve forzare nulla, secondo la sua definizione h _ = ().

osservazioni di separazione: la pigrizia può avere ragion d'essere ma non è la sua definizione. La pigrizia è quello che è. È definito come tutti i valori che inizialmente sono thunk che sono costretti a WHNF in base alle richieste provenienti da main. Se aiuta a evitare il fondo in un determinato caso specifico in base alle sue circostanze specifiche, lo fa. Altrimenti no. Questo è tutto.

Aiuta a implementarlo da solo, nella tua lingua preferita, per farti un'idea. Ma possiamo anche tracciare la valutazione di qualsiasi espressione nominando attentamente tutti i valori provvisori man mano che nascono.


Seguendo il rapporto , abbiamo

f :: a -> ()
f = \_ -> (undefined :: ())

poi

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

e con

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

continua

 = putStrLn (case (undefined :: ()) of () -> "()")

Ora, dice la sezione 3.17.3 Semantica formale del pattern matching del rapporto

La semantica delle caseespressioni [sono fornite] nelle Figure 3.1–3.3. Qualsiasi implementazione dovrebbe comportarsi in modo tale che queste identità [...] valgano.

e caso (r)negli stati della Figura 3.2

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() è costruttore di dati di arity 0, quindi è lo stesso di

(r)     case  of { () -> e; _ -> e } =  

e il risultato complessivo della valutazione è quindi .


2
Mi piace la tua spiegazione. È chiaro e semplice
arrowd

@DanielWagner In realtà avevo in mente il caseCore, e stavo ignorando il buco. :) Ho modificato per menzionare il Core.
Will Ness,

1
La forzatura non sarebbe showinvocata da print? Qualcosa di simileshow x = case x of () -> "()"
user253751

1
Mi riferisco a caseCore, non a Haskell stesso. Haskell è tradotto in Core, che ha un forzante case, AFAIK. Hai ragione sul fatto che casein Haskell non sta forzando da solo. Potrei scrivere qualcosa in Scheme o ML (se potessi scrivere ML che è), o pseudocodice.
Will Ness,

1
La risposta autorevole a tutto ciò è probabilmente da qualche parte nel Rapporto. Tutto quello che so è che non c'è "ottimizzazione" in corso qui e "valore regolare" non è un termine significativo in questo contesto. Qualunque cosa sia forzata, forzata. printforza quanto basta per stampare. non guarda il tipo, i tipi sono spariti, cancellati, quando il programma viene eseguito, la subroutine di stampa corretta è già scelta e compilata, in base al tipo, al momento della compilazione; tale subroutine forzerà comunque il suo valore di input su WHNF in fase di esecuzione e, se non definito, causerà un errore.
Will Ness,
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.