La monade IO è tecnicamente errata?


12

Sul wiki di haskell c'è il seguente esempio di uso condizionale della monade IO (vedi qui) .

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

Si noti che in questo esempio, la definizione di IO aè presa RealWorld -> (a, RealWorld)per rendere tutto più comprensibile.

Questo frammento esegue in modo condizionale un'azione nella monade IO. Ora, supponendo che conditionsia False, l'azione actionnon dovrebbe mai essere eseguita. Usando la semantica pigra questo sarebbe davvero il caso. Tuttavia, si fa notare qui che Haskell è tecnicamente parlando non rigorosa. Ciò significa che al compilatore è consentito, ad esempio, eseguire preventivamente action worldsu un thread diverso e in seguito buttare via quel calcolo quando scopre che non è necessario. Tuttavia, a quel punto gli effetti collaterali saranno già avvenuti.

Ora, si potrebbe implementare la monade IO in modo tale che gli effetti collaterali vengano propagati solo quando l'intero programma è terminato, e sappiamo esattamente quali effetti collaterali dovrebbero essere eseguiti. Questo non è il caso, tuttavia, poiché è possibile scrivere programmi infiniti in Haskell, che hanno chiaramente effetti collaterali intermedi.

Questo significa che la monade IO è tecnicamente sbagliata o c'è qualcos'altro che impedisce che ciò accada?


Benvenuti in Informatica ! La tua domanda è fuori tema qui: ci occupiamo di domande di informatica , non di domande di programmazione (vedi le nostre FAQ ). La tua domanda potrebbe essere in argomento su StackTranslate.it .
dkaeae

2
A mio avviso si tratta di una domanda di informatica, poiché si occupa della semantica teorica di Haskell, non di una questione di programmazione pratica.
Lasse,

4
Non ho molta familiarità con la teoria del linguaggio di programmazione, ma penso che questa domanda sia sull'argomento qui. Potrebbe essere utile chiarire che cosa significa "sbagliato" qui. Quale proprietà pensi che la monade IO abbia che non dovrebbe avere?
Lucertola discreta

1
Questo programma non è ben scritto. Non sono sicuro di cosa intendevi scrivere. La definizione di whenè tipizzabile, ma non ha il tipo che dichiari e non vedo cosa rende interessante questo particolare codice.
Gilles 'SO- smetti di essere malvagio'

2
Questo programma è preso alla lettera dalla pagina wiki di Haskell collegata direttamente sopra. In effetti non digita. Questo perché è scritto nell'ipotesi IO adefinita come RealWorld -> (a, RealWorld), al fine di rendere più leggibili gli interni di IO.
Lasse,

Risposte:


12

Questa è una "interpretazione" suggerita della IOmonade. Se vuoi prendere sul serio questa "interpretazione", allora devi prendere sul serio "RealWorld". Non importa se action worldviene valutato in modo speculativo o meno, actionnon ha effetti collaterali, i suoi effetti, se presenti, vengono gestiti restituendo un nuovo stato dell'universo in cui si sono verificati quegli effetti, ad esempio un pacchetto di rete è stato inviato. Tuttavia, il risultato della funzione è ((),world)e quindi è il nuovo stato dell'universo world. Non usiamo il nuovo universo che potremmo aver valutato speculativamente sul lato. Lo stato dell'universo è world.

Probabilmente hai difficoltà a prenderlo sul serio. Ci sono molti modi in cui ciò è superficialmente paradossale e senza senso. La concorrenza è particolarmente ovvia o folle con questa prospettiva.

"Aspetta, aspetta" dici. " RealWorldè solo un" token ". In realtà non è lo stato dell'intero universo." Bene, allora questa "interpretazione" non spiega nulla. Tuttavia, come dettaglio di implementazione , ecco come i modelli GHC IO. 1 Tuttavia, ciò significa che abbiamo "funzioni" magiche che effettivamente hanno effetti collaterali e questo modello non fornisce alcuna guida al loro significato. E, dal momento che queste funzioni hanno effetti collaterali, la preoccupazione che sollevi è del tutto chiara. GHC non deve andare fuori del suo modo per assicurarsi che RealWorlde queste funzioni speciali non sono ottimizzati in modi che cambiano il comportamento previsto del programma.

Personalmente (come probabilmente è ormai evidente), penso che questo modello "di passaggio del mondo" IOsia solo inutile e confuso come strumento pedagogico. (Se sia utile per l'implementazione, non lo so. Per GHC, penso che sia più un manufatto storico.)

Un approccio alternativo è quello di visualizzare IOle richieste descrittive con i gestori delle risposte. Esistono diversi modi per farlo. Probabilmente il più accessibile è usare una costruzione di monade gratuita, in particolare possiamo usare:

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

Esistono molti modi per renderlo più sofisticato e avere proprietà leggermente migliori, ma questo è già un miglioramento. Non richiede profonde ipotesi filosofiche sulla natura della realtà per capire. Tutto ciò che afferma è che IOè un programma banale Returnche non fa altro che restituire un valore, oppure è una richiesta al sistema operativo con un gestore per la risposta. OSRequestpuò essere qualcosa del tipo:

data OSRequest = OpenFile FilePath | PutStr String | ...

Allo stesso modo, OSResponsepotrebbe essere qualcosa di simile:

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(Uno dei miglioramenti che è possibile apportare è rendere le cose più sicure in modo da sapere che non si otterrà OpenSucceededda una PutStrrichiesta.) Questo modello IOdescrive le richieste che vengono interpretate da un sistema (per la IOmonade "reale" questo è il runtime Haskell stesso), e quindi, forse, quel sistema chiamerà il gestore a cui abbiamo fornito una risposta. Questo, ovviamente, non fornisce alcuna indicazione su come gestire una richiesta simile PutStr "hello world", ma non pretende di farlo. Rende esplicito che questo viene delegato ad un altro sistema. Anche questo modello è abbastanza preciso. Tutti i programmi utente nei moderni sistemi operativi devono fare richieste al sistema operativo per fare qualsiasi cosa.

Questo modello fornisce le giuste intuizioni. Ad esempio, molti principianti vedono cose come l' <-operatore come "da scartare" IO, o hanno (purtroppo rafforzato) punti di vista che un IO String, diciamo, è un "contenitore" che "contiene" Strings (e poi <-li fa uscire). Questa visione richiesta-risposta rende chiaramente sbagliata questa prospettiva. Non esiste un handle di file all'interno di OpenFile "foo" (\r -> ...). Un'analogia comune per sottolineare questo è che non esiste una torta all'interno di una ricetta per torta (o forse "fattura" sarebbe meglio in questo caso).

Questo modello funziona anche facilmente con la concorrenza. Possiamo facilmente avere un costruttore per OSRequestlike Fork :: (OSResponse -> IO ()) -> OSRequeste quindi il runtime può intercalare le richieste prodotte da questo gestore extra con il gestore normale come preferisce. Con un po 'di intelligenza puoi usare questo (o tecniche correlate) per modellare cose come la concorrenza più direttamente piuttosto che dire semplicemente "facciamo una richiesta al sistema operativo e le cose accadono". Ecco come funziona la IOSpecbiblioteca .

1 Hugs ha utilizzato un'implementazione basata sulla continuazione, IOche è approssimativamente simile a ciò che descrivo anche se con funzioni opache anziché un tipo di dati esplicito. HBC ha anche utilizzato un'implementazione basata sulla continuazione stratificata sul vecchio IO basato sul flusso di richiesta-risposta. NHC (e quindi YHC) ha usato i thunk, cioè approssimativamente IO a = () -> asebbene sia ()stato chiamato World, ma non sta facendo passare lo stato. JHC e UHC hanno utilizzato sostanzialmente lo stesso approccio di GHC.


Grazie per la tua risposta illuminante, mi ha davvero aiutato. La tua implementazione di IO ha richiesto del tempo per farmi capire, ma sono d'accordo che sia più intuitivo. Stai sostenendo che questa implementazione non soffre di potenziali problemi con l'ordinamento degli effetti collaterali come fa l'implementazione di RealWorld? Non riesco immediatamente a vedere alcun problema, ma non mi è nemmeno chiaro che non esistano.
Lasse,

Un commento: sembra che OpenFile "foo" (\r -> ...)dovrebbe essere effettivamente Request (OpenFile "foo") (\r -> ...)?
Lasse,

@Lasse Sì, avrebbe dovuto essere con Request. Per rispondere alla tua prima domanda, questo IOè chiaramente insensibile all'ordine di valutazione (fondi del modulo) perché è un valore inerte. Tutti gli effetti collaterali (se presenti) sarebbero generati dalla cosa che interpreta questo valore. Nel whenesempio, non avrebbe importanza se actionè stata valutata, perché sarebbe solo un valore simile Request (PutStr "foo") (...), che non daremo alla cosa interpretare queste richieste in ogni caso. È come il codice sorgente; non importa se lo riduci avidamente o pigramente, non succede nulla finché non viene dato a un interprete.
Derek Elkins lasciò

Ah sì, lo vedo. Questa è una definizione davvero intelligente. All'inizio ho pensato che tutti gli effetti collaterali dovevano necessariamente verificarsi quando l'intero programma ha terminato l'esecuzione, perché devi costruire la struttura dati prima di poterla interpretare. Ma poiché una richiesta contiene una continuazione, devi solo creare i dati del primo Requestper iniziare a vedere gli effetti collaterali. Gli effetti collaterali successivi possono essere creati durante la valutazione della continuazione. Intelligente!
Lasse,
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.