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

17

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

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

Ho anche capito che l'approccio di Haskell a questo, la monade IO, lo raggiunge avvolgendo azioni di stato in un contenitore, per un'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 le specifiche 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 del monade IO non è efficace in questo modo:

steps = list(
    read_file,
    convert,
    write_file,
)

quindi assolvere la responsabilità non effettivamente chiamando quei passaggi, ma lasciando che sia l'interprete a farlo?

In altre parole, è 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 aspettati che qualcun altro chiami inner() e dichiari che il tuo lavoro è finito perché main() è puro.

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

Quando il codice è effettivamente eseguito , tutto ciò che segue la lettura del file dipende dallo stato di quel file, quindi continuerà a soffrire degli stessi bug legati allo stato dell'implementazione imperativa, quindi hai effettivamente guadagnato qualcosa, come programmatore che manterrà questo?

Apprezzo pienamente il vantaggio di ridurre e isolare il comportamento di stato, che è in effetti il motivo per cui ho strutturato la versione imperativa del genere: raccogliere input, fare roba pura, sputare uscite Speriamo che convert() possa essere completamente puro e sfruttare i vantaggi di cachability, threadsafety, ecc.

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

C'è qualche ulteriore vantaggio nell'affrontare gli effetti collaterali che porta il pattern di monade IO, che mi manca?

    
posta Stu Cox 06.08.2015 - 01:43
fonte

4 risposte

14

The whole program is going to end up contained in the IO monad, basically.

Questo è il punto in cui penso che tu non lo stia vedendo 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 che convert , la parte pura:

  1. Probabilmente è la maggior parte di questo programma, e di gran lunga più complicato delle parti IO ;
  2. Può essere ragionato e testato senza dover affrontare IO .

Quindi non vedono questo come convert che è "contenuto" in IO , ma piuttosto, essendo isolato da IO . Dal suo tipo, qualunque cosa convert non può mai dipendere da tutto ciò che accade in un'azione IO .

When the code is actually executed, everything after reading the file depends on that file’s state so will still suffer from the same state-related bugs as the imperative implementation, so have you actually gained anything, as a programmer who will maintain this?

Direi che questo si divide in due cose:

  1. Quando viene eseguito il programma, il valore dell'argomento su convert dipende dallo stato del file.
  2. Ma ciò che la funzione convert fa , non dipende dallo stato del file. convert è sempre la stessa funzione , anche se è invocato con argomenti diversi in punti diversi.

Questo è un punto un po 'astratto, ma è davvero la chiave di quello che Haskellers intende quando parlano di questo. Vuoi scrivere convert in modo tale che, dato l'argomento valido qualsiasi , produrrà un risultato corretto per quell'argomento. Quando lo guardi in quel modo, il fatto che leggere un file sia un'operazione statica non entra nell'equazione; tutto ciò che conta è che qualunque argomento gli venga dato e da dove possa provenire, convert deve gestirlo correttamente. E il fatto che la purezza limiti ciò che convert può fare con il suo input semplifica questo ragionamento.

Quindi se convert produce risultati errati da alcuni argomenti, e readFile lo alimenta come argomento, non lo vediamo come un bug introdotto da stato . È un bug in una pura funzione!

    
risposta data 12.09.2015 - 20:49
fonte
7

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

Come spiegato in Tackling the Awkward Squad di Simon Peyton Jones ( strongmente lettura consigliata!), l'I / O monadico era destinato a risolvere problemi reali con il modo in cui Haskell usava gestire 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 quando esegui la programmazione in Python per conformarti a questo stile, ma per quanto riguarda i moduli che non hai scritto? Senza molto aiuto dal sistema di tipi (e dalle librerie comuni), l'I / O monadico è probabilmente meno utile in Python. La filosofia del linguaggio non ha lo scopo di imporre una rigorosa separazione pura / impura.

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

Un'altra cosa. Tu dici:

The whole program is going to end up contained in the IO monad, basically.

È vero che la funzione haskell main "vive" in IO , ma i programmi Haskell reali sono incoraggiati a non utilizzare IO ogni volta che non è necessario. Quasi ogni funzione che scrivi che non ha bisogno di fare I / O non dovrebbe avere tipo IO .

Quindi direi che nel tuo ultimo esempio hai ottenuto il backward: main è impuro (perché legge e scrive file) ma le funzioni di base come convert sono pure.

    
risposta data 06.08.2015 - 04:15
fonte
3

Perché è impuro IO? Perché potrebbe restituire valori diversi in momenti diversi. Esiste una dipendenza nel tempo in cui deve essere tenuto in conto, in un modo o nell'altro. Questo è ancora più cruciale con la 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 dovrebbe mai essere visualizzato? Non c'è nulla che dipende da questo, quindi una valutazione pigra significa che non verrà mai richiesta. Inoltre, non vi è nulla di convincente che il prompt venga 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, name impone un ordine sui secondi due.

Ci sono altri modi per risolvere il problema della dipendenza dagli ordini, ma l'utilizzo di una monade IO è probabilmente il modo più semplice (almeno dal punto di vista della lingua) per consentire a tutto di rimanere nel reame funzionale pigro, senza piccole sezioni di codice imperativo. È anche il più flessibile. Ad esempio, è possibile creare in modo relativamente semplice una pipeline di I / O in fase di esecuzione in base all'input dell'utente.

    
risposta data 06.08.2015 - 05:51
fonte
1

My (limited) understanding of functional programming is that state/side effects should be minimised and kept separate from stateless logic.

Questa non è solo programmazione funzionale; di solito è una buona idea in qualsiasi lingua. Se esegui test unitari, il modo in cui ti dividi read_file() , convert() e 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 facile: tutto ciò che devi impostare è il parametro di input. Scrivere test per read_file() e write_file() è un po 'più difficile (anche se le funzioni stesse sono quasi banali) perché è necessario creare e / o leggere le cose sul file system prima e dopo aver chiamato la funzione. Idealmente, realizzerai tali funzioni in modo così semplice che ti sentirai a loro agio non testarle e quindi risparmiare un sacco di problemi.

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 hai bisogno di sperare che nessuno sia caduto accidentalmente in una funzione di lettura o scrittura di file in convert() (ad esempio, read_config_file() ). In Haskell quando si dichiara convert :: String -> String o simile, senza IO monad, il controllo di tipo garantirà che questa è una funzione pura che si basa solo sul suo parametro di input e nient'altro. Se qualcuno tenta di modificare convert per leggere un file di configurazione, vedrà rapidamente gli errori del compilatore che dimostrano che violerebbero la purezza della funzione. (E si spera che siano abbastanza sensibili da spostare read_config_file su convert e passare il suo risultato in convert , mantenendo la purezza.)

    
risposta data 06.12.2018 - 03:22
fonte

Leggi altre domande sui tag