Alternativa alla purezza della lingua

7

Purezza

Uno dei concetti interessanti di Haskell è la purezza. Tuttavia, mi chiedo quali siano le ragioni pragmatiche alla base di questo - lasciami spiegare un po 'di più prima di rifiutare la mia domanda.

Il mio punto principale è che avremo effetti collaterali indipendentemente da come possiamo incapsularli in una lingua. Quindi, anche se un linguaggio di programmazione come Haskell è puro, stiamo semplicemente "posticipando" gli effetti collaterali fino a un momento successivo.

Capisco l'idea che la purezza renda più semplici i ragionamenti sui programmi (trasparenza referenziale e così via). In Haskell, incapsuliamo tutti gli effetti collaterali nel sistema di tipi (la monade IO). In quanto tale, non possiamo avere un effetto collaterale senza propagare la monade IO attraverso i tipi di tutte le funzioni che chiamano la nostra funzione impura, per così dire.

In quanto tale, sono molto interessato all'idea di separare le pure funzioni da quelle impure nel sistema dei tipi. Questo sembra un buon modo per costringerci a limitare tali effetti.

Tuttavia, Haskell viene spesso colpito a causa della sua natura pura - le persone dicono che non è pratico o pragmatico. Se questo è vero o no è un'altra questione, ma il fatto è che molte persone rifiutano Haskell per questo motivo.

Un approccio alternativo? (chiamiamolo "tipo inquinamento")

Per me, sembra che il principale punto vincente non sia la purezza in sé, ma la separazione tra codice puro e impuro. Se incapsulare IO in una monade è troppo poco pratico, perché non proviamo un approccio più semplice?

Cosa succede se invece etichettiamo ogni tipo di ritorno e tipo di argomento nel sistema come puro o impuro. Il linguaggio D ha una parola chiave "pura" esplicita, ma penso che lasciamo che le funzioni siano pure di default e segnino solo le funzioni impure.

In un linguaggio simile a ML questo potrebbe apparire come questo:

let square x = x * x

let impure getDbData connectionString = ...

let impure getMappedDbData mapping =
    let dbData = getDbData "<some connection info>"
    in map mapping dbData

let main =  // Compiler error, as it is not marked as impure
    let mappedData = getMappedData square
    in ...

Inoltre, chiamare qualsiasi funzione impura "inquinerebbe" il tipo di ritorno delle funzioni di chiamata, rendendolo impuro. Se non fosse etichettato come tale, darebbe un errore di compilazione.

(naturalmente, passare le funzioni come argomenti dovrebbe essere verificato rispetto alla purezza prevista, ma dovrebbe essere facile dedurre la "purezza" prevista degli argomenti)

(anche l'etichetta "impura" potrebbe essere dedotta, suppongo che sia meglio mantenere questo esplicito, per essere leggibile)

Le funzioni pure d'altra parte possono essere trasmesse ovunque, indipendentemente dalla purezza prevista. Potremmo dare una funzione pura come input per una funzione che accetta uno impuro. Quindi non è una relazione simmetrica - quindi la parola "inquinamento".

Tutto sommato, questo esplicitamente (e controllato staticamente) separa la base di codice in una parte pura e impura. In molti modi, è vicino alla funzionalità di una monade IO (anche se l'asimmetria probabilmente esclude una mappatura 1-a-1 tra i due), ma con un involucro implicito fatto automaticamente. Ancora più importante, molti programmatori lo troverebbero più intuitivo e pragmatico di una monade IO esplicita o qualcosa di simile.

E a differenza delle soluzioni completamente impure, questo dà molta più sicurezza alla ragione riguardo alle parti pure (senza stare troppo a nostro agio nel definire quelle impure). E tutto sommato, è soprattutto una versione applicata staticamente di ciò che i programmatori più funzionali considerano comunque la migliore pratica (la separazione).

TL; DR;

Qualcosa come l'approccio sopra descritto è mai stato provato? Se no, c'è qualche ragione per cui (sia tecniche, pratiche, teoriche e filosofiche sono interessanti)?

E, infine, un tale controllo della purezza sarebbe più facile da accettare per le persone che affermano che Haskell non è pratico?

responsabilità

Personalmente non nutro rancore verso Haskell e la scelta di utilizzare le monadi per l'IO. In effetti, trovo che Haskell sia uno dei linguaggi più eleganti in uso e forse l'unica lingua che mi piace di più.

Inoltre, è chiaro che l'approccio sopra menzionato non offre qualcosa che la monade IO non fa - l'unica cosa è che esso è più strettamente (visivamente) simile a linguaggi impuri come OCaml, F # e così via, che è legato a rendilo più facile da capire per molte persone. In effetti, prendere il codice F # esistente e aggiungere alcune etichette "impure" sarebbe l'unica differenza visiva del codice.

Quindi, questo potrebbe essere un facile passo verso la purezza per le lingue che non hanno lo stesso ecosistema che circonda le monadi come Haskell.

    
posta nilu 03.10.2014 - 20:45
fonte

2 risposte

14

Hai descritto un sistema di effetti . È vero che esistono altri sistemi di effetti rispetto alle monadi, ma in pratica le monadi ti danno un sacco di potere espressivo che avresti bisogno di reinventare in qualsiasi sistema di effetti pratici che potresti escogitare.

Ad esempio:

  • Inizio taggando le mie procedure I / O con un effetto io .
  • Le mie pure funzioni possono ancora generare eccezioni, ma il lancio di eccezioni non è io , quindi aggiungo un effetto exn . Ora devo essere in grado di combinare gli effetti.
  • Voglio avere funzioni non I / O che utilizzano la mutazione internamente su un heap locale h , e voglio che il compilatore si assicuri che i miei riferimenti mutabili non sfuggano al loro scope, quindi aggiungo un% co_de effetto%. Ora ho bisogno di effetti parametrizzati.
  • Per le funzioni di ordine superiore, ho bisogno di scrivere più versioni per ogni combinazione di effetti che desidero supportare, ad esempio st<h> con una funzione pura contro map con una funzione impura. Ora ho bisogno di polimorfismo dell'effetto.
  • Ho notato che il mio tipo definito dall'utente potrebbe essere usato per modellare gli effetti (benign failure, nondeterminism) che il linguaggio non ha in modo nativo. Ora ho bisogno di effetti definiti dall'utente.

Le Monade supportano tutti questi casi d'uso:

  • Le funzioni che possono restituire errori possono utilizzare map monad.
  • Gli effetti possono essere combinati con i trasformatori monad: se ho bisogno di eccezioni e I / O, posso usare Either monad.
  • Le Monade possono essere parametrizzate come qualsiasi altro tipo; il EitherT IO monad ti consente di fare la mutazione locale su ST h s nell'ambito di un heap STRef .
  • A causa della% typ_cart class%, posso sovraccaricare un'operazione per lavorare su qualsiasi tipo di effetto. Ad esempio, h funziona in Monad , ma funziona anche in mapM :: (Monad m) => (a -> m b) -> [a] -> m [b] o IO o in qualsiasi altra monade.
  • Tutte le monadi sono implementate come tipi definiti dall'utente, ad eccezione di cose come Either e ST che devono essere implementate nel runtime di Haskell.

Ovviamente ci sono alcune verruche significative nell'uso che Haskell fa delle monadi per gli effetti collaterali.

Per prima cosa, i trasformatori monad non sono generalmente commutativi, quindi IO è diverso da ST , e devi stare attento a scegliere la combinazione che intendi realmente.

Il problema più grande è che una funzione può essere polimorfa in cui viene usata la monade, ma non può essere polimorfa in se viene usata una monade. Quindi otteniamo un sacco di codice duplicato: StateT Maybe vs. MaybeT State ; map vs. mapM , & c.

Ora, in risposta alla tua domanda attuale ...

Koka di Microsoft Research è un linguaggio con un sistema di effetti che non soffre di questi problemi . Ad esempio, il tipo di funzione zipWith di Koka include un parametro di tipo zipWithM per gli effetti:

map : (xs : list<a>, f : (a) -> e b) -> e list<b>

E questo parametro può essere istanziato all'effetto null, facendo sì che map funzioni ugualmente per le funzioni pure e impure. Inoltre, l'ordine in cui vengono specificati gli effetti è irrilevante: e è uguale a map .

Vale anche la pena ricordare che il meccanismo delle eccezioni controllate di Java è un esempio di un sistema di effetti che le persone hanno ampiamente accettato, ma non ritengo che risponda adeguatamente alla tua domanda perché non è generale.

    
risposta data 04.10.2014 - 01:42
fonte
10

Penso che tu abbia reinventato le monadi! Diamo un'occhiata a ciò che abbiamo qui, possiamo "sporcare" implicitamente un puro calcolo e usarlo in uno impuro, e possiamo chiamare la funzione impura da un'altra impura.

Sembra molto monade, possiamo sporcare un valore puro con return , per chiamare un'altra funzione da una funzione impura, possiamo semplicemente usare >>=

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

Ora il tuo approccio è un po 'più familiare alle persone di altre lingue: tutti noi abbiamo una sorta di controllo di purezza che attraversa le nostre teste per cercare di capire cosa sta succedendo dove.

Lo svantaggio è che rendendo queste cose un concetto di livello linguistico, ci siamo resi molto difficili da sviluppare. In Haskell esiste un'intera libreria di funzioni normali definite dall'utente che funzionano su tutte le monadi. Se abbiamo cotto IO all'ingrosso nella lingua, saremmo costretti a rendere primitive tutte queste funzioni!

Riducendo al minimo la quantità di magia oscura intorno al modo in cui viene gestita la purezza, possiamo effettivamente rendere le cose più facili da gestire da parte degli utenti.

    
risposta data 03.10.2014 - 22:20
fonte