2009-12-16 14 views
17

Ho cercato di riprendere la programmazione funzionale per un po 'di tempo? Ho cercato il calcolo lambda, LISP, OCML, F # e persino la logica combinatoria, ma il problema principale che ho è come si fanno cose che richiedono effetti collaterali come (interagire con un utente, comunicare con un servizio remoto, o anche gestire la simulazione usando campionamento casuale) senza violare la premessa fondamentale della pura programmazione funzionale che è, per un dato input, l'output è deterministico? Spero di avere un senso, se non mi piace qualsiasi tentativo di educarmi adeguatamente. Grazie in anticipo.Gli effetti collaterali sono possibili nella pura programmazione funzionale

+2

A proposito, il titolo della domanda e la domanda sono molto diversi. Potresti voler cambiare il titolo in "Gli effetti collaterali sono possibili in un linguaggio puramente funzionale?" –

risposta

20

La maggior parte della programmazione funzionale del mondo reale non è "pura" nella maggior parte dei sensi, quindi metà della risposta alla tua domanda è "lo fai rinunciando alla purezza". Detto questo, ci sono alternative.

Nel senso "puro" di puro, l'intero programma rappresenta una singola funzione di uno o più argomenti, restituendo un valore. Se strizzi gli occhi e agiti un po 'le mani, puoi dichiarare che tutti gli input dell'utente sono parte degli "argomenti" della funzione e che tutto l'output fa parte del "valore di ritorno" e quindi fudge leggermente le cose in modo che faccia solo l'effettivo I/O "su richiesta".

Una prospettiva simile è dichiarare che l'input della funzione è "l'intero stato del mondo esterno" e che la valutazione della funzione restituisce un nuovo "stato del mondo" modificato. In tal caso, qualsiasi funzione del programma che utilizza lo stato mondiale viene ovviamente liberata dall'essere "deterministica" poiché nessuna valutazione del programma avrà esattamente lo stesso mondo esterno.

Se si voleva scrivere un programma interattivo nel puro lambda calcolo (o qualcosa di equivalente, come il linguaggio esoterico Lazy K), concettualmente come si farebbe.

In termini più pratici, il problema consiste nel fare in modo che l'I/O si verifichi nell'ordine corretto quando l'input viene utilizzato come argomento di una funzione. La struttura generale della soluzione "pura" a questo problema è la composizione della funzione . Ad esempio, supponiamo di avere tre funzioni che eseguono l'I/O e di chiamarle in un determinato ordine. Se fai qualcosa come RunThreeFunctions(f1, f2, f3) non c'è nulla per determinare l'ordine in cui saranno valutati. D'altra parte, se lasci che ogni funzione assuma un'altra funzione come argomento, puoi concatenarli in questo modo: f1(f2(f3())), nel qual caso sai che f3 verrà valutato per primo perché la valutazione di f2 dipende dal suo valore.[Modifica: Vedere anche il commento qui sotto sulla valutazione pigra vs desiderosa. Questo è importante, perché la valutazione pigra è in realtà abbastanza comune in contesti molto puri; ad es., l'implementazione standard della ricorsione nel calcolo lambda puro non sta terminando sotto stimolante valutazione.]

Ancora, per scrivere un programma interattivo nel calcolo lambda, è così che probabilmente lo faresti. Se si desidera qualcosa effettivamente utilizzabile per la programmazione in, probabilmente si vorrà combinare la parte della composizione della funzione con la struttura concettuale delle funzioni prendendo e restituendo valori che rappresentano lo stato del mondo e creare un'astrazione di ordine superiore per gestire il pipelining del " "stato mondiale" valori tra le funzioni di I/O, idealmente anche mantenendo lo "stato mondiale" contenuto al fine di imporre una rigorosa linearità - a quel punto hai quasi reinventato la Monad di Haskell IO.

Speriamo che questo non ti abbia solo reso ancora più confuso.

+4

"D'altra parte, se lasci che ogni funzione assuma un'altra funzione come argomento, puoi concatenarle in questo modo: f1 (f2 (f3())), nel qual caso sai che f3 sarà valutato prima perché la valutazione di f2 dipende dal suo valore. " - solo se 'f2' e' f1' usano effettivamente il suo argomento per calcolare il suo risultato, e solo se anche il chiamante di 'f1' userà il suo risultato. Altrimenti la valutazione pigra può legittimamente dare il calcio d'inizio. –

+5

Ovviamente siete corretti, e ho sorvolato molti altri dettagli allo scopo di non esacerbare la verbosità già prodigiosa della risposta. –

+0

Quindi lo stato del mondo cambia mentre le funzioni vengono valutate? Ciò che riscrive la struttura dello stato del mondo è al di fuori dell'applicazione? Devi continuare a chiamare la tua applicazione in un ciclo per affrontare il cambiamento dello stato del mondo? –

5

La programmazione funzionale riguarda la limitazione degli effetti secondari di isolamento &, non tentando di eliminarli completamente ... perché non è possibile.

... e sì, trovo FP utile (certamente con Erlang comunque): trovo che sia più facile passare da "idea" a "programma" (o problema a soluzione;) ... ma ovviamente ciò potrebbe solo essere me.

+0

Anch'io, mi sono sempre piaciuti i concetti di pipelining e filtering come funzioni di base per l'elaborazione dei dati, che è un principio fondamentale della programmazione funzionale in generale. –

+0

@Jeremy E: d'accordo: vengo da un ambiente di ingegneria HW e molto spesso penso in termini di "flussi di dati", proprio come me lo consente Erlang. – jldupont

2

È necessario conoscere almeno un altro concetto essenziale: Monads. Avrai bisogno di questo per fare I/O e l'altra roba "utile"!

0

Anche se non lo si utilizza nel proprio lavoro, l'apprendimento di uno o più linguaggi di programmazione funzionali è un ottimo modo per imparare a pensare in modo diverso e fornisce un kit di approcci alternativi ai problemi (può anche frustrare quando si non può fare qualcosa di pulito e pulito come un approccio funzionale in altre lingue).

E mi ha reso migliore nella stesura dei fogli di stile XSL.

+1

XSLT: Perché l'XML completo di Turing è esattamente ciò di cui il mondo ha bisogno. –

+0

@camccann Spero che tu sia sarcastico. –

+0

Trovo che il XML completo di Turing sia ampiamente preferibile al meccanismo di generalizzazione del tipo completo di Turing (c.f. modelli C++, ecc.). Detto questo, per chi odia le parentesi angolari, c'è XQuery. –

9

Haskell è un puro linguaggio di programmazione funzionale. In Haskell tutte le funzioni sono pure (cioè danno sempre la stessa uscita per gli stessi ingressi). Ma come gestisci gli effetti collaterali in Haskell? Bene, questo problema è risolto magnificamente attraverso l'uso di monads.

Assunzione di I/O come esempio. In Haskell ogni funzione che esegue I/O restituisce un calcolo IO, cioè un calcolo nella monade IO. Così, per esempio, una funzione che legge un int dalla tastiera, invece di restituire un int, restituisce un calcolo IO che produce un int quando viene eseguito:

askForInt :: String -> IO Int 

perché restituisce un calcolo di I/O, invece di un Int, non è possibile utilizzare questo risultato direttamente in una somma, ad esempio. Per accedere al valore Int è necessario "scartare" il calcolo. L'unico modo per farlo è quello di utilizzare la funzione bind (>>=):

(>>=) :: IO a -> (a -> IO b) -> IO b 

Poiché questo restituisce anche un calcolo IO, si finisce sempre con un calcolo di I/O. Questo è il modo in cui Haskell isola gli effetti collaterali. La monade IO agisce come un'astrazione dello stato del mondo reale (infatti sotto le copertine viene solitamente implementato con un tipo chiamato RealWorld per la parte stato).

+0

Ma poiché le monadi sono non-funzionali, Haskell non è * un "puro linguaggio di programmazione funzionale". – RBarryYoung

+2

@RBarryYoung: cosa intendi per "non funzione"? Cosa ti fa dire che Haskell non è puramente funzionale? Haskell * è * puramente funzionale. – Stephan202

+2

Essere un puro linguaggio funzionale non significa che tutto sia una funzione. È un ** linguaggio ** funzionale e tutte le funzioni sono ** pure ** (non importa 'unsafePerformIO') –

2

Il modo in cui Haskell utilizza le monade è wikipedia e la spiegazione di Haskell sul numero page.

Fondamentalmente l'idea è di non sbarazzarsi della monade IO. La mia comprensione è che siete in grado di concatenare funzioni che scartano una monade IO ed eseguono quella funzione. Ma non sei in grado di rimuovere del tutto la monade IO.

Un altro esempio di monade che non è direttamente legato a IO è il forse Monad. Questa monade è "non confezionabile" in contrasto con la monade IO.Ma è più facile spiegare l'uso delle monadi usando la monade Forse. Supponiamo che tu abbia la seguente funzione.

wrap :: Maybe x -> (x -> y) -> Maybe y 
wrap Nothing f = Nothing 
wrap (Just x) f = Just (f x) 

ora è possibile chiamare wrap (Just 4) (5+) che restituirà Just 9.

L'idea del monio IO è che è possibile utilizzare funzioni come (+5) sul tipo interno. La monade assicurerà che le funzioni saranno chiamate in serie, perché ogni funzione è incatenata con il wrapping IO-monad.

+1

Questa è l'idea. C'è una funzione/operatore di bind ('>> =') che "estrae" un calcolo IO e lo passa a una funzione che restituisce * un altro * calcolo IO. Perché non c'è altro modo per "scartare" un calcolo IO, non puoi sbarazzartene. –

+0

@Ruben: 'unwrappable' => Per riferimento futuro, queste monadi sono chiamate monad aperte, invece che IO è una monade chiusa. –

+0

grazie. Non conoscevo il termine. – Ruben

7

L'interazione con un utente e la comunicazione con un servizio remoto richiedono una parte non funzionale del software.

Molti "linguaggi funzionali" (come la maggior parte dei Lisps) non sono puramente funzionale. Ti lasciano comunque fare cose con effetti collaterali, anche se le cose con effetti collaterali sono "scoraggiate" nella maggior parte dei contesti.

Haskell è "puramente funzionale" ma consente comunque di eseguire operazioni non funzionali tramite la monade IO. L'idea di base è che il tuo programma puramente funzionale emetta una struttura di dati lazy che viene valutata da un programma non funzionale (che non scrivi, fa parte dell'ambiente). Si potrebbe sostenere che questa stessa struttura dati sia un programma imperativo. Quindi stai facendo una meta-programmazione imperativa in un linguaggio funzionale.

Ignorando quale approccio è "migliore", l'obiettivo in entrambi i casi è quello di creare una separazione tra le parti funzionali e non funzionali dei programmi e limitare il più possibile le dimensioni delle parti non funzionali. Le parti funzionali tendono ad essere più riutilizzabili, testabili e più facili da ragionare.

+0

+1: punto di vista interessante. – gorsky

2

L'unico linguaggio funzionale che conosco è il sistema di template in C++. Haskell occupa il secondo posto rendendo esplicite le parti imperative del programma.

In Haskell il programma ha uno stato mutabile, ma le funzioni (quasi sempre) non lo fanno. Tieni come il 99% percento del programma puro, e solo la porzione che interagisce con il mondo esterno è impura. Quindi quando stai testando una funzione, sai che non ci sono effetti collaterali. Nucleo puro, con una conchiglia impura.

+0

Stavo iniziando a venire alla stessa conclusione da solo tutti, aiutandomi a chiarire questo. –

+0

Haskell non ha uno stato mutabile: "falsa" usando la definizione "newtype StateT s m a = StateT {runStateT :: s -> m (a, s)}" e IO ha una definizione quasi identica. –

1

Dato che la maggior parte dei programmi ha alcuni effetti sul mondo esterno (scrittura di file, modifica di dati in un database ...) i programmi nel loro complesso raramente sono privi di effetti collaterali. Al di fuori degli esercizi accademici, non ha senso nemmeno provare.

Ma i programmi sono assemblati su blocchi predefiniti (subroutine, funzione, metodo, chiamatelo come volete), e le funzioni pure creano blocchi di costruzione molto ben funzionanti.

La maggior parte dei linguaggi di programmazione funzionale non richiede che le funzioni siano pure, sebbene i buoni programmatori funzionanti cercheranno di rendere il più possibile delle loro funzioni il più possibile e pratico, al fine di cogliere i vantaggi della trasparenza referenziale.

Haskell va oltre. Ogni parte di un programma Haskell è pura (almeno in assenza di peccati come "unsafePerformIO"). Tutte le funzioni che scrivi in ​​Haskell sono pure.

Gli effetti collaterali vengono introdotti attraverso le monadi. Possono essere usati per introdurre una sorta di "shopping-list-shopper" -separation. Essenzialmente il tuo programma scrive una lista della spesa (che è solo dati e può essere manipolata in modo puro), mentre il runtime della lingua interpreta la lista della spesa e fa lo shopping efficace. Tutto il tuo codice è puro e amichevole per ragionamenti equazionali e simili, mentre il codice impuro è fornito dai compilatori-scrittori.

Problemi correlati