Come funziona la funzione di immutabilità della Programmazione funzionale con CQS?

3

Data l'immutabilità (che è spesso incoraggiata e si dice che sia uno degli elementi costitutivi della programmazione funzionale) e CQS (che dice che i comandi non dovrebbero restituire un valore diverso da void / unit), come funzionano insieme?

Ad esempio:

Come eseguiresti una serie di comandi (ad esempio più copie di file, che possono lanciare tutti i tipi di eccezioni ma non dovrebbero mandare in crash il programma) e raccogliere il loro successo o fallimento + la ragione in un elenco di risultati?

Posso immaginare alcune soluzioni in OO come lanciare eventi o dare accesso a un'istanza di Report (sia statica che passata per riferimento), ma quali sono le possibilità nei linguaggi funzionali che non interrompono CQS?

    
posta koenmetsu 17.08.2015 - 08:22
fonte

3 risposte

8

Given immutability (which is often encouraged and said to be one of building blocks of functional programming) and CQS (which says that commands should not return a value other than void/unit), how do these work together?

Non ci sono comandi nella programmazione funzionale. Periodo.

Il risultato di una funzione può dipendere solo dai suoi argomenti, e deve essere lo stesso ogni volta che si chiama la funzione con gli stessi argomenti. Non ci possono essere mutazioni né effetti collaterali. Le funzioni devono essere referenzialmente trasparenti, il che significa che posso sempre sostituire l'applicazione della funzione con il suo risultato ovunque nel programma senza cambiare il significato del programma. (In particolare, questo significa che se ho una procedura che restituisce void (cioè niente), posso sostituirla con niente ovunque, che ovviamente cambierà il significato del programma, e quindi una procedura non può esistere nella programmazione funzionale.)

Ovviamente, qualcosa come un comando deve essere ancora realizzabile, altrimenti i tuoi programmi sarebbero piuttosto noiosi. Come hanno detto Simon Peyton-Jones durante l'introduzione a Haskell, se il tuo programma non ha effetti collaterali, non può stampare un risultato, non può chiedere input, non può interagire con la rete o il file system, tutto ciò che fa è il calore la CPU, dopo la quale qualcuno del pubblico ha interposto che il riscaldamento della CPU è anche un effetto collaterale, quindi, tecnicamente parlando, se vuoi che il tuo programma non abbia effetti collaterali, non puoi nemmeno eseguirlo. Chiaramente, è ridicolo.

Quindi, gli effetti collaterali devono essere modellati in qualche modo.

Un modo per farlo è immaginare di poter descrivere in qualche modo lo stato dell'intero universo in una struttura dati e quindi passare un'istanza di quella struttura di dati che descrive lo stato corrente del mondo in un funzione, e la funzione restituisce quindi una nuova istanza di tale infrastruttura che descrive il nuovo stato del mondo. Naturalmente, questo non può letteralmente lavorare, descrivere lo stato del mondo richiede almeno la stessa quantità di memoria di tutto il mondo. È un modello concettuale.

C'è un piccolo problema: la funzione potrebbe passare il vecchio stato del mondo a un'altra funzione, ad esempio, ora ci sono due funzioni che hanno lo stesso stato del mondo, ed entrambe potrebbero restituire un nuovo stato diverso del mondo. Oppure, una funzione potrebbe aggrapparsi al vecchio stato del mondo, e in seguito tornare indietro, viaggiando in modo efficace nel tempo nel passato. Certo, questo deve essere in qualche modo proibito. Una soluzione sarebbe costituita da tipi lineari, che sono tipi che assicurano che i suoi valori vengano utilizzati una sola volta. Pulito fa questo, ha tipi di mondo, che sono un tipo speciale di tipi lineari per tali valori mondiali.

In Haskell, tutti gli effetti collaterali devono aver luogo in una monade. Le Monade sono un modo per aumentare il calcolo con una struttura aggiuntiva. Potete immaginarlo come la monade che trasmette il valore del mondo descritto sopra attraverso le diverse funzioni, ma senza mai esporre direttamente il valore alle funzioni. (In realtà, una volta che si dispone di I / O monodico, non si ha più bisogno del valore del mondo, la monade stessa fornisce la struttura per gli effetti collaterali.) I precedenti tentativi di modellazione I / O in Haskell includevano l'I / O basato su flussi pigri e I / O basati su continuazione, che funzionano anche, ma è stato trovato che l'I / O monadico è più facile da utilizzare.

In tutti questi modelli diversi, gli effetti collaterali verranno sempre visualizzati nei tipi di funzioni di effetti collaterali. IOW, i tuoi comandi ora sono funzioni che restituiscono, accettano un argomento, o entrambi, un valore di un tipo a effetto collaterale.

Un bell'effetto collaterale (hah!) di questo è che diversi tipi di effetti collaterali possono ottenere i propri tipi, invece di avere un tipo "tutto questo è un effetto collaterale". Ad esempio, in Haskell, esiste il ben noto tipo IO , che in pratica dice "tutto può succedere". Ma ci sono anche tipi come Reader (legge lo stato globale, un esempio OO sarebbe un oggetto di configurazione globale Singleton che è immesso dalla dipendenza), Writer (scrive lo stato globale, ad esempio un logger, di nuovo, tipicamente risolto in OO con un oggetto logger globale singleton che viene iniettato), State (stato condiviso mutevole, ma nessun altro tipo di effetto collaterale), STM ( Memoria transazionale software , come State ma transazionale per parallelismo).

La porzione "separazione" di CQS viene raggiunta utilizzando il sistema di tipi. I comandi hanno semplicemente tipi incompatibili con le query. Cioè un "comando" che cambia qualche intero globale avrà State Int da qualche parte nel suo tipo, un comando che stampa un intero avrà tipo Int -> IO Int e così via.

    
risposta data 17.08.2015 - 13:56
fonte
5

CQS è un idioma di linguaggi orientati agli oggetti progettato per evitare la confusione che può essere causata quando una funzione muta inaspettatamente lo stato o interagisce con l'ambiente.

Questo non è un problema per un linguaggio funzionale. Nei linguaggi funzionali, tutte queste interazioni sono esplicite nelle firme delle funzioni coinvolte. Quindi, CQS non ha alcuno scopo in un linguaggio funzionale: i comandi sono necessariamente separati a causa del fatto che entrambi devono consumare e restituire un oggetto che rappresenta la parte dell'ambiente con cui interagiscono. Questo oggetto è in genere una monade e tale monade può essere utilizzata per molti scopi, incluso l'accumulo di risultati ed errori.

    
risposta data 17.08.2015 - 09:23
fonte
3

In Haskell, i comandi sono valori di prima classe. Considera la funzione copyFile dal modulo System.Directory . La sua firma è questa:

copyFile :: FilePath -> FilePath -> IO ()

copyFile è una funzione che accetta due FilePath s come argomenti e restituisce un comando (digita IO () ). Questo comando, una volta eseguito, copia il file dalla prima posizione alla seconda. Quindi nota che:

  1. La funzione non copia il file. Restituisce solo un comando.
  2. Valutare un'espressione e eseguire un comando sono cose diverse in Haskell.
  3. Haskell è orientato al comando che ha comandi al posto di istruzioni o blocchi. È così orientato ai comandi che un programma eseguibile Haskell è un comando, scritto in Haskell, che il compilatore si traduce in codice oggetto.

Poiché i comandi sono valori di prima classe, puoi creare un elenco di comandi. Supponiamo di avere un elenco di coppie sorgente / destinazione:

locations :: [(FilePath, FilePath)]
locations = [("source1", "dest1"), ("source2", "dest2"), ...]

Usando map puoi trasformarlo in un elenco di comandi che copiano i file:

copyCommands :: [IO ()]
copyCommands = map (\(from, to) -> copyFile from to) locations

E Haskell ha funzioni standard per la creazione di comandi complessi da elenchi di comandi; ad esempio, la funzione sequence_ che converte una lista di comandi in un comando che esegue i contenuti in sequenza.

copyFiles :: [(FilePath, FilePath)] -> IO ()
copyFiles = sequence_ . map (\(from, to) -> copyFile from to)

O, scritto in modo idiomatico:

copyFiles :: [(FilePath, FilePath)] -> IO ()
copyFiles = for_ (uncurry copyFile)

Questo utilizza due funzioni di utilità standard:

for_ toAction values = sequence_ (map toAction values)

uncurry f (a, b) = f a b

Questo non è ancora ciò che vuoi, tuttavia, perché:

  • Non raccoglie gli errori che si verificano durante le copie dei file;
  • Uscirà dalla copia dei file quando il primo fallisce.

Ma è facile da risolvere. Per prima cosa scriviamo un wrapper attorno a copyFile che cattura le sue eccezioni e avvolge il risultato in un tipo Either (usando tryIO da Control.Error.Util per rilevare le eccezioni e runExceptT per scartarlo dal trasformatore monade):

tryCopyFile :: FilePath -> FilePath -> IO (Either IOException ())
tryCopyFile from to = runExceptT (tryIO (copyFile from to))

E ora dato un elenco di coppie (from, to) , possiamo semplicemente usare la funzione standard traverse per costruire un comando che, una volta eseguito, esegue i singoli comandi e l'elenco dei nostri risultati (errori e successi):

tryCopyFiles :: [(FilePath, FilePath)] -> IO [Either Exception ()]
tryCopyFiles = traverse (uncurry tryCopyFile)

Nota il tipo IO [Either IOException ()] ; questo è il tipo di comandi ( IO ... ) che, quando eseguiti, producono una lista ( [...] ) i cui elementi sono o IOException o unità.

Quindi la risposta è che, una volta superata la curva di apprendimento molto ripida, è veramente facile .

    
risposta data 12.09.2015 - 03:28
fonte

Leggi altre domande sui tag