Il vantaggio del modello di monade IO per la gestione degli effetti collaterali è puramente accademico?


17

Ci scusiamo per l'ennesima domanda sugli effetti collaterali di FP +, ma non sono riuscito a trovarne uno esistente che mi abbia risposto.

La mia (limitata) comprensione della programmazione funzionale è che gli effetti di stato / collaterali dovrebbero essere minimizzati e tenuti separati dalla logica senza stato.

Raccolgo anche l'approccio di Haskell a questo, la monade IO, lo realizza avvolgendo le azioni stateful in un container, per l'esecuzione successiva, considerata al di fuori dell'ambito del programma stesso.

Sto cercando di capire questo schema, ma in realtà per determinare se usarlo in un progetto Python, quindi voglio evitare i dettagli di Haskell se possibile.

Esempio grezzo in arrivo.

Se il mio programma converte un file XML in un file JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

L'approccio della monade IO non è efficace nel fare questo:

steps = list(
    read_file,
    convert,
    write_file,
)

quindi assolvere se stesso dalla responsabilità non chiamando effettivamente quei passaggi, ma lasciando che l'interprete lo faccia?

O in altri termini, è come scrivere:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

quindi aspettandomi che qualcun altro chiami inner()e dica che il tuo lavoro è fatto perché main()è puro.

L'intero programma finirà per essere contenuto nella monade IO, in pratica.

Quando il codice viene effettivamente eseguito , tutto dopo aver letto il file dipende dallo stato di quel file, quindi soffrirà ancora degli stessi bug relativi allo stato dell'implementazione imperativa, quindi hai effettivamente guadagnato qualcosa, come programmatore che manterrà questo?

Apprezzo totalmente il vantaggio di ridurre e isolare il comportamento con stato, motivo per cui ho strutturato la versione imperativa in questo modo: raccogliere input, fare cose pure, sputare output. Si spera che convert()possa essere completamente puro e raccogliere i benefici di cachability, thread-safety, ecc.

Apprezzo anche che i tipi monadici possano essere utili, specialmente nelle condotte che operano su tipi comparabili, ma non vedo perché l'IO dovrebbe usare le monadi se non già in una tale conduttura.

C'è qualche ulteriore vantaggio nel gestire gli effetti collaterali che porta il modello di monade IO, che mi manca?


1
Dovresti guardare questo video . Le meraviglie delle monadi vengono finalmente svelate senza ricorrere alla teoria delle categorie o a Haskell. Si scopre che le monadi sono banalmente espresse in JavaScript e sono uno dei fattori abilitanti chiave dell'Ajax. Le monadi sono fantastiche. Sono cose semplici, quasi banalmente implementate, con un enorme potere di gestire la complessità. Ma comprenderli è sorprendentemente difficile e la maggior parte delle persone, una volta che hanno quel momento ah-ah, sembrano perdere la capacità di spiegarli agli altri.
Robert Harvey,

Bel video, grazie. In realtà ho appreso queste cose da un'introduzione JS alla programmazione funzionale (quindi ho letto un milione in più ...). Anche se l'ho visto, sono abbastanza sicuro che la mia domanda sia specifica per la monade IO, che Crock non copre in quel video.
Stu Cox,

Hmm ... AJAX non è considerato una forma di I / O?
Robert Harvey,

1
Si noti che il tipo di mainin un programma Haskell è IO ()- un'azione IO. Questa in realtà non è affatto una funzione; è un valore . L'intero programma è un valore puro contenente istruzioni che indicano al runtime della lingua cosa dovrebbe fare. Tutte le cose impure (che eseguono effettivamente le azioni IO) non rientrano nell'ambito del programma.
Wyzard

Nel tuo esempio, la parte monadica è quando devi prendere il risultato di un calcolo ( read_file) e usarlo come argomento per quello successivo ( write_file). Se avessi solo una sequenza di azioni indipendenti, non avresti bisogno di una Monade.
Lortabac,

Risposte:


14

L'intero programma finirà per essere contenuto nella monade IO, in pratica.

Questo è il punto in cui penso che non lo vedi dal punto di vista degli Haskeller. Quindi abbiamo un programma come questo:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Penso che una tipica interpretazione di Haskeller su questo sarebbe quella convert, la parte pura:

  1. È probabilmente il grosso di questo programma, e di gran lunga più complicato delle IOparti;
  2. Può essere ragionato e testato senza doverlo affrontare IOaffatto.

Quindi non vedono questo come convertessere "contenuta" in IO, ma piuttosto, come essendo isolato da IO. Dal suo tipo, qualunque cosa convertfaccia non può mai dipendere da qualsiasi cosa accada in IOun'azione.

Quando il codice viene effettivamente eseguito, tutto dopo aver letto il file dipende dallo stato di quel file, quindi soffrirà ancora degli stessi bug relativi allo stato dell'implementazione imperativa, quindi hai effettivamente guadagnato qualcosa, come programmatore che manterrà questo?

Direi che questo si divide in due cose:

  1. Quando il programma viene eseguito, il valore della discussione alla convertdipende dallo stato del file.
  2. Ma ciò che la convertfunzione di fa , che non dipende dallo stato del file. convertè sempre la stessa funzione , anche se viene invocato con argomenti diversi in punti diversi.

Questo è un punto un po 'astratto, ma è davvero la chiave di ciò che gli Haskeller intendono quando ne parlano. Si desidera scrivere convertin modo tale che, dato qualsiasi argomento valido, produca un risultato corretto per tale argomento. Quando lo guardi in quel modo, il fatto che leggere un file sia un'operazione con stato non entra nell'equazione; tutto ciò che conta è che qualunque argomento gli sia nutrito e da qualunque parte possa provenire, convertdeve gestirlo correttamente. E il fatto che la purezza limiti ciò che convertpuò fare con il suo input semplifica tale ragionamento.

Quindi, se convertproduce risultati errati da alcuni argomenti e lo readFilealimenta come tale argomento, non lo vediamo come un bug introdotto dallo stato . È un bug in una funzione pura!


Penso che questa sia la migliore descrizione (anche se gli altri hanno aiutato a chiarire le cose anche per me), grazie.
Stu Cox,

vale la pena notare che l'uso delle monadi in Python potrebbe avere meno benefici in quanto Python ha solo un tipo (statico), e quindi non dare garanzie su nulla?
jk.

7

È difficile essere sicuri di cosa esattamente intendi per "puramente accademico", ma penso che la risposta sia principalmente "no".

Come spiegato in Tackling the Awkward Squad di Simon Peyton Jones ( lettura fortemente consigliata!), L'I / O monadico doveva risolvere problemi reali con il modo in cui Haskell gestiva l'I / O. Leggi l'esempio del server con Richieste e risposte, che non copierò qui; è molto istruttivo.

Haskell, a differenza di Python, incoraggia uno stile di calcolo "puro" che può essere applicato dal suo sistema di tipi. Ovviamente, puoi usare l'autodisciplina durante la programmazione in Python per rispettare questo stile, ma per quanto riguarda i moduli che non hai scritto? Senza molto aiuto dal sistema dei tipi (e dalle librerie comuni), l'I / O monadico è probabilmente meno utile in Python. La filosofia del linguaggio non intende semplicemente imporre una rigorosa separazione pura / impura.

Si noti che questo dice di più sulle diverse filosofie di Haskell e Python che su come sia l'I / O monadico accademico. Non lo userei per Python.

Un'altra cosa Tu dici:

L'intero programma finirà per essere contenuto nella monade IO, in pratica.

È vero che la mainfunzione Haskell "vive" IO, ma i veri programmi Haskell sono incoraggiati a non usare IOogni volta che non è necessario. Quasi ogni funzione che scrivi che non ha bisogno di fare I / O non dovrebbe avere il tipo IO.

Quindi direi nel tuo ultimo esempio che l'hai ottenuto al contrario: mainè impuro (perché legge e scrive file) ma funzioni di base come convertsono pure.


3

Perché IO è impuro? Perché può restituire valori diversi in momenti diversi. C'è una dipendenza nel tempo che deve essere spiegata, in un modo o nell'altro. Questo è ancora più cruciale con una valutazione pigra. Considera il seguente programma:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Senza una monade IO, perché il primo prompt otterrebbe mai l'output? Non c'è nulla a seconda di ciò, quindi una valutazione pigra significa che non verrà mai richiesto. Non c'è inoltre nulla di convincente che il prompt sia emesso prima che l'input venga letto. Per quanto riguarda il computer, senza una monade IO, quelle prime due espressioni sono completamente indipendenti l'una dall'altra. Fortunatamente, nameimpone un ordine sui secondi due.

Esistono altri modi per risolvere il problema della dipendenza dall'ordine, ma l'uso di una monade IO è probabilmente il modo più semplice (almeno dal punto di vista linguistico) per consentire a tutto di rimanere nel regno pigro funzionale, senza piccole sezioni di codice imperativo. È anche il più flessibile. Ad esempio, è possibile creare relativamente facilmente una pipeline IO in modo dinamico in fase di esecuzione in base all'input dell'utente.


2

La mia (limitata) comprensione della programmazione funzionale è che gli effetti di stato / collaterali dovrebbero essere minimizzati e tenuti separati dalla logica senza stato.

Non è solo una programmazione funzionale; di solito è una buona idea in qualsiasi lingua. Se esegui test di unità, il modo in cui ti dividi read_file(), convert()ed write_file()è perfettamente naturale perché, nonostante convert()sia di gran lunga la parte più complessa e più grande del codice, scrivere test per questo è relativamente semplice: tutto ciò che devi impostare è il parametro di input . Scrivere test per read_file()ed write_file()è un po 'più difficile (anche se le funzioni stesse sono quasi banali) perché è necessario creare e / o leggere cose sul file system prima e dopo aver chiamato la funzione. Idealmente, renderebbe tali funzioni così semplici da farti sentire a tuo agio nel non testarle, risparmiando così molta seccatura.

La differenza tra Python e Haskell qui è che Haskell ha un controllo di tipo che può dimostrare che le funzioni non hanno effetti collaterali. In Python devi sperare che nessuno sia caduto accidentalmente in una funzione di lettura o scrittura di file in convert()(diciamo, read_config_file()). In Haskell quando dichiari convert :: String -> Stringo simili, con noIO monade, il controllo del tipo ti garantirà che questa è una funzione pura che si basa solo sul suo parametro di input e nient'altro. Se qualcuno tenta di modificare convertper leggere un file di configurazione, vedrà rapidamente errori del compilatore che mostrano che avrebbero infranto la purezza della funzione. (E si spera che sarebbero abbastanza ragionevoli da read_config_fileabbandonare converte passare il risultato in convert, mantenendo la purezza.)

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.