Come funzionano i linguaggi di programmazione funzionale?


92

Se i linguaggi di programmazione funzionale non possono salvare nessuno stato, come fanno cose semplici come leggere l'input di un utente? Come "memorizzano" l'input (o memorizzano i dati per quella materia?)

Ad esempio: come si tradurrebbe questa semplice cosa in C in un linguaggio di programmazione funzionale come Haskell?

#include<stdio.h>
int main() {
    int no;
    scanf("%d",&no);
    return 0;
}

(La mia domanda è stata ispirata da questo eccellente post: "Esecuzione nel regno dei nomi" . La lettura mi ha dato una migliore comprensione di cosa sia esattamente la programmazione orientata agli oggetti, di come Java la implementa in un modo estremo e di come i linguaggi di programmazione funzionali siano un contrasto.)



4
È una buona domanda, perché a livello sistemico un computer ha bisogno di uno stato per essere utile. Ho visto un'intervista con Simon Peyton-Jones (uno degli sviluppatori dietro Haskell) in cui diceva che un computer che eseguiva solo software completamente apolidi poteva realizzare solo una cosa: diventare caldo! Molte buone risposte di seguito. Ci sono due strategie principali: 1) Crea un linguaggio impuro. 2) Prepara un piano astuto per astrarre lo stato, che è quello che fa Haskell, essenzialmente creando un nuovo mondo leggermente cambiato invece di modificare quello vecchio.
danneggia

14
SPJ non stava parlando di effetti collaterali lì, non di stato? I calcoli puri hanno molti stati impliciti nelle associazioni di argomenti e nello stack di chiamate, ma senza effetti collaterali (ad esempio, I / O) non possono fare nulla di utile. I due punti sono davvero molto distinti: ci sono tonnellate di codice Haskell puro e con stato, e la Statemonade è molto elegante; d'altra parte IOè un brutto, sporco trucco usato solo di malavoglia.
CA McCann

4
camccann ha ragione. C'è molto stato nei linguaggi funzionali. È solo gestito esplicitamente invece di "azioni spettrali a distanza" come nelle lingue imperative.
SOLO LA MIA CORRETTA OPINIONE

1
Potrebbe esserci un po 'di confusione qui. Forse i computer hanno bisogno di effetti per essere utili, ma penso che la domanda qui riguardi i linguaggi di programmazione, non i computer.
Conal

Risposte:


80

Se i linguaggi di programmazione funzionale non possono salvare alcuno stato, come fanno alcune cose semplici come leggere l'input di un utente (intendo dire come lo "memorizzano") o memorizzano i dati per quella materia?

Come hai capito, la programmazione funzionale non ha uno stato, ma ciò non significa che non possa memorizzare dati. La differenza è che se scrivo un'affermazione (Haskell) sulla falsariga di

let x = func value 3.14 20 "random"
in ...

Sono garantito che il valore di xè sempre lo stesso in ...: nulla può eventualmente cambiarlo. Allo stesso modo, se ho una funzione f :: String -> Integer(una funzione che prende una stringa e restituisce un numero intero), posso essere certo che fnon modificherà il suo argomento, né cambierà alcuna variabile globale, né scriverà dati su un file e così via. Come ha detto sepp2k in un commento sopra, questa non mutabilità è davvero utile per ragionare sui programmi: scrivi funzioni che piegano, mandano e mutilano i tuoi dati, restituendo nuove copie in modo da poterle concatenare insieme, e puoi essere sicuro che nessuna di quelle chiamate di funzione possono fare qualsiasi cosa "dannosa". Sai che xè sempre x, e non devi preoccuparti che qualcuno abbia scritto x := foo barda qualche parte tra la dichiarazione dix e il suo utilizzo, perché è impossibile.

Ora, cosa succede se voglio leggere l'input di un utente? Come ha detto KennyTM, l'idea è che una funzione impura sia una funzione pura che viene passata al mondo intero come argomento e restituisce sia il suo risultato che il mondo. Certo, non vuoi farlo davvero: per prima cosa, è orribilmente goffo, e per un altro, cosa succede se riutilizzo lo stesso oggetto del mondo? Quindi questo viene astratto in qualche modo. Haskell lo gestisce con il tipo IO:

main :: IO ()
main = do str <- getLine
          let no = fst . head $ reads str :: Integer
          ...

Questo ci dice che mainè un'azione IO che non restituisce nulla; eseguire questa azione è ciò che significa eseguire un programma Haskell. La regola è che i tipi di I / O non possono mai sfuggire a un'azione I / O; in questo contesto, introduciamo tale azione utilizzando do. Pertanto, getLinerestituisce un IO String, che può essere pensato in due modi: primo, come un'azione che, quando eseguita, produce una stringa; secondo, come una stringa "contaminata" da IO poiché è stata ottenuta in modo impuro. Il primo è più corretto, ma il secondo può essere più utile. Il <-prende il Stringfuori IO Stringe lo memorizza in str-ma visto che siamo in un'azione IO, dovremo avvolgerlo il backup, in modo da non può "fuga". La riga successiva tenta di leggere un numero intero ( reads) e acquisisce la prima corrispondenza riuscita (fst . head); questo è tutto puro (no IO), quindi gli diamo un nome con let no = .... Possiamo quindi utilizzare sia noe strin .... Abbiamo quindi memorizzato dati impuri (da getLineinto str) e dati puri ( let no = ...).

Questo meccanismo per lavorare con l'IO è molto potente: ti consente di separare la parte pura e algoritmica del tuo programma dal lato impuro dell'interazione con l'utente e di applicarlo a livello di tipo. La tua minimumSpanningTreefunzione non può cambiare qualcosa da qualche altra parte nel tuo codice, o scrivere un messaggio per il tuo utente e così via. É sicuro.

Questo è tutto ciò che devi sapere per utilizzare IO in Haskell; se è tutto quello che vuoi, puoi fermarti qui. Ma se vuoi capire perché funziona, continua a leggere. (E nota che questa roba sarà specifica per Haskell: altre lingue potrebbero scegliere un'implementazione diversa.)

Quindi questo probabilmente sembrava un po 'un trucco, in qualche modo aggiungendo impurità al puro Haskell. Ma non lo è: si scopre che possiamo implementare il tipo di I / O interamente all'interno di Haskell puro (purché ci venga fornito il RealWorld). L'idea è questa: un'azione IO IO typeè la stessa di una funzione RealWorld -> (type, RealWorld), che prende il mondo reale e restituisce sia un oggetto di tipo typeche quello modificato RealWorld. Definiamo quindi un paio di funzioni in modo da poter utilizzare questo tipo senza impazzire:

return :: a -> IO a
return a = \rw -> (a,rw)

(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'

Il primo ci permette di parlare di azioni IO che non fanno nulla: return 3è un'azione IO che non interroga il mondo reale e si limita a restituire 3. L' >>=operatore, pronunciato "bind", ci permette di eseguire azioni IO. Estrae il valore dall'azione IO, lo passa e il mondo reale attraverso la funzione e restituisce l'azione IO risultante. Nota che >>=applica la nostra regola secondo cui i risultati delle azioni IO non possono mai sfuggire.

Possiamo quindi trasformare quanto sopra mainnel seguente insieme ordinario di applicazioni di funzioni:

main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...

Il runtime Haskell inizia maincon l'iniziale RealWorlde siamo pronti! Tutto è puro, ha solo una sintassi stravagante.

[ Modifica: come sottolinea @Conal , questo non è in realtà ciò che Haskell usa per fare IO. Questo modello si interrompe se si aggiunge la concorrenza, o addirittura un modo in cui il mondo cambia nel bel mezzo di un'azione di I / O, quindi sarebbe impossibile per Haskell utilizzare questo modello. È accurato solo per il calcolo sequenziale. Quindi, può essere che l'IO di Haskell sia un po 'una schivata; anche se non lo è, non è certo così elegante. L'osservazione di Per @ Conal, guarda cosa dice Simon Peyton-Jones in Tackling the Awkward Squad [pdf] , sezione 3.1; presenta quello che potrebbe equivalere a un modello alternativo lungo queste linee, ma poi lo abbandona per la sua complessità e prende una strada diversa.]

Di nuovo, questo spiega (più o meno) come l'IO e la mutabilità in generale funzionano in Haskell; se questo è tutto ciò che vuoi sapere, puoi smettere di leggere qui. Se vuoi un'ultima dose di teoria, continua a leggere, ma ricorda, a questo punto, siamo andati molto lontano dalla tua domanda!

Quindi l'ultima cosa: risulta che questa struttura - un tipo parametrico con returne >>=- è molto generale; si chiama monade, e donotazione return, e >>=funziona con ognuna di esse. Come hai visto qui, le monadi non sono magiche; tutto ciò che è magico è che i doblocchi si trasformano in chiamate di funzione. Il RealWorldtipo è l'unico posto in cui vediamo la magia. Anche tipi come []il costruttore della lista sono monadi e non hanno nulla a che fare con il codice impuro.

Ora sai (quasi) tutto sul concetto di monade (tranne alcune leggi che devono essere soddisfatte e la definizione matematica formale), ma ti manca l'intuizione. Ci sono un numero ridicolo di tutorial sulle monadi online; Mi piace questo , ma hai delle opzioni. Tuttavia, questo probabilmente non ti aiuterà ; l'unico vero modo per ottenere l'intuizione è attraverso una combinazione di utilizzarli e leggere un paio di tutorial al momento giusto.

Tuttavia, non hai bisogno di quell'intuizione per capire IO . Comprendere le monadi in piena generalità è la ciliegina sulla torta, ma puoi usare IO adesso. Potresti usarlo dopo che ti ho mostrato la prima mainfunzione. Puoi anche trattare il codice IO come se fosse in un linguaggio impuro! Ma ricorda che c'è una rappresentazione funzionale sottostante: nessuno bara.

(PS: scusa per la lunghezza. Sono andato un po 'lontano.)


6
La cosa che mi colpisce sempre di Haskell (che ho fatto e sto facendo sforzi coraggiosi per imparare) è la bruttezza della sintassi. È come se avessero preso i pezzi peggiori di ogni altra lingua, li avessero buttati in un secchio e si fossero mossi furiosamente. E queste persone si lamenteranno della sintassi certamente (in alcuni punti) strana del C ++!

19
Neil: Davvero? In realtà trovo la sintassi di Haskell molto pulita. Sono curioso; a cosa ti riferisci in particolare? (Per quel che vale, il C ++ non mi dà fastidio, tranne che per la necessità di farlo > >nei modelli.)
Antal Spector-Zabusky

6
A mio avviso, mentre la sintassi di Haskell non è così pulita come, ad esempio, Scheme, non inizia a essere paragonabile all'orribile sintassi di, beh, anche il più carino dei linguaggi parentesi graffe, di cui il C ++ è tra i peggiori . Nessuna considerazione per il gusto, suppongo. Non credo che esista una lingua che tutti trovino sintatticamente piacevole.
CA McCann

8
@ NeilButterworth: sospetto che il tuo problema non sia tanto la sintassi quanto i nomi delle funzioni. Se le funzioni come >>=o $avessero più dove invece chiamassero binde apply, il codice haskell sarebbe molto meno simile a perl. Voglio dire, la principale differenza tra haskell e la sintassi dello schema è che haskell ha operatori infissi e parentesi opzionali. Se le persone si astenessero dall'usare eccessivamente gli operatori infix, haskell assomiglierebbe molto a uno schema con meno parentesi.
sepp2k

5
@camcann: Bene, punto, ma quello che volevo dire è: La sintassi di base dello schema è (functionName arg1 arg2). Se rimuovi le parentesi, functionName arg1 arg2è la sintassi haskell. Se permetti gli operatori infissi con nomi arbitrariamente orribili, ottieni arg1 §$%&/*°^? arg2che è ancora più simile a haskell. (Sto solo scherzando, in realtà mi piace haskell).
sepp2k

23

Molte buone risposte qui, ma sono lunghe. Proverò a dare una breve risposta utile:

  • I linguaggi funzionali mettono lo stato negli stessi posti del C: nelle variabili denominate e negli oggetti allocati sull'heap. Le differenze sono che:

    • In un linguaggio funzionale, una "variabile" ottiene il suo valore iniziale quando entra nello scope (tramite una chiamata di funzione o un let-binding), e quel valore non cambia in seguito . Allo stesso modo, un oggetto allocato sull'heap viene immediatamente inizializzato con i valori di tutti i suoi campi, che non cambiano in seguito.

    • I "cambiamenti di stato" non vengono gestiti modificando variabili o oggetti esistenti, ma legando nuove variabili o allocando nuovi oggetti.

  • IO funziona con un trucco. Un calcolo con effetti collaterali che produce una stringa è descritto da una funzione che accetta un World come argomento e restituisce una coppia contenente la stringa e un nuovo World. The World include il contenuto di tutte le unità disco, la cronologia di ogni pacchetto di rete mai inviato o ricevuto, il colore di ogni pixel sullo schermo e cose del genere. La chiave del trucco è che l'accesso al mondo è attentamente limitato in modo che

    • Nessun programma può fare una copia del mondo (dove la metteresti?)

    • Nessun programma può buttare via il mondo

    L'uso di questo trucco rende possibile l'esistenza di un mondo unico, il cui stato si evolve nel tempo. Il sistema di run-time del linguaggio, che non è scritto in un linguaggio funzionale, implementa un calcolo con effetti collaterali aggiornando l'unico World in posizione invece di restituirne uno nuovo.

    Questo trucco è magnificamente spiegato da Simon Peyton Jones e Phil Wadler nel loro documento fondamentale "Programmazione Funzionale Imperativa" .


4
Per quanto ne so, questa IOstoria ( World -> (a,World)) è un mito quando applicata ad Haskell, poiché quel modello spiega solo il calcolo puramente sequenziale, mentre il IOtipo di Haskell include la concorrenza. Con "puramente sequenziale", intendo che nemmeno il mondo (universo) può cambiare tra l'inizio e la fine di un calcolo imperativo, se non a causa di quel calcolo. Ad esempio, mentre il tuo computer sta soffocando, il tuo cervello ecc. Non può. La concorrenza può essere gestita da qualcosa di più simile World -> PowerSet [(a,World)], che consente il non determinismo e l'interleaving.
Conal

1
@Conal: penso che la storia di IO generalizzi abbastanza bene al non determinismo e all'interleaving; se ricordo bene, c'è una spiegazione abbastanza buona nel documento "Awkward Squad". Ma non conosco un buon articolo che spieghi chiaramente il vero parallelismo.
Norman Ramsey

3
A quanto ho capito, il documento "Awkward Squad" abbandona il tentativo di generalizzare il semplice modello denotazionale di IO, ie World -> (a,World)(il "mito" popolare e persistente a cui mi riferivo) e fornisce invece una spiegazione operativa. Ad alcune persone piace la semantica operazionale, ma mi lasciano completamente insoddisfatto. Si prega di vedere la mia risposta più lunga in un'altra risposta.
Conal

+1 Questo mi ha aiutato a capire molto di più le monadi IO e a rispondere alla domanda.
CaptainCasey

La maggior parte dei compilatori Haskell effettivamente definisce IOcome RealWorld -> (a,RealWorld), ma invece di rappresentare effettivamente il mondo reale è solo un valore astratto che deve essere passato e finisce per essere ottimizzato dal compilatore.
Jeremy List

19

Interrompo la risposta di un commento a una nuova risposta, per dare più spazio:

Scrissi:

Per quanto ne so, questa IOstoria ( World -> (a,World)) è un mito quando applicata ad Haskell, poiché quel modello spiega solo il calcolo puramente sequenziale, mentre il IOtipo di Haskell include la concorrenza. Con "puramente sequenziale", intendo che nemmeno il mondo (universo) può cambiare tra l'inizio e la fine di un calcolo imperativo, se non a causa di quel calcolo. Ad esempio, mentre il tuo computer sta soffocando, il tuo cervello ecc. Non può. La concorrenza può essere gestita da qualcosa di più simile World -> PowerSet [(a,World)], che consente il non determinismo e l'interleaving.

Norman ha scritto:

@Conal: penso che la storia di IO generalizzi abbastanza bene al non determinismo e all'interleaving; se ricordo bene, c'è una spiegazione abbastanza buona nel documento "Awkward Squad". Ma non conosco un buon articolo che spieghi chiaramente il vero parallelismo.

@ Norman: Generalizza in che senso? Sto suggerendo che il modello / spiegazione denotazionale solitamente fornito, World -> (a,World)non corrisponde a Haskell IOperché non tiene conto del non determinismo e della concorrenza. Potrebbe esserci un modello più complesso che si adatta, come World -> PowerSet [(a,World)], ma non so se un tale modello sia stato elaborato e mostrato adeguato e coerente. Personalmente dubito che una tale bestia possa essere trovata, dato che IOè popolata da migliaia di chiamate API imperative importate da FFI. E come tale, IOsta adempiendo al suo scopo:

Problema aperto: la IOmonade è diventata il peccato di Haskell. (Ogni volta che non capiamo qualcosa, lo lanciamo nella monade IO.)

(Dal discorso POPL di Simon PJ Wearing the hair shirt Wearing the hair shirt: a retrospective on Haskell .)

Nella sezione 3.1 di Tackling the Awkward Squad , Simon indica cosa non funziona type IO a = World -> (a, World), incluso "L'approccio non scala bene quando si aggiunge la concorrenza". Quindi suggerisce un possibile modello alternativo, e poi abbandona il tentativo di spiegazioni denotazionali, dicendo

Tuttavia adotteremo invece una semantica operazionale, basata su approcci standard alla semantica dei calcoli di processo.

Questa incapacità di trovare un modello denotazionale preciso e utile è alla radice del motivo per cui vedo Haskell IO come un allontanamento dallo spirito e dai profondi benefici di ciò che chiamiamo "programmazione funzionale", o ciò che Peter Landin ha più specificamente chiamato "programmazione denotativa" . Vedi commenti qui.


Grazie per la risposta più lunga. Penso che forse i nostri nuovi signori operativi mi hanno fatto il lavaggio del cervello. I motori a sinistra e quelli a destra e così via hanno permesso di dimostrare alcuni utili teoremi. Hai visto qualche modello denotazionale che ti piace che tenga conto del non determinismo e della concorrenza? No.
Norman Ramsey

1
Mi piace il modo in cui World -> PowerSet [World]cattura nitidamente il non determinismo e la concorrenza in stile interleaving. Questa definizione di dominio mi dice che la programmazione imperativa concorrente tradizionale (inclusa quella di Haskell) è intrattabile, letteralmente esponenzialmente più complessa che sequenziale. Il grande danno che vedo nel IOmito Haskell è oscurare questa complessità intrinseca, demotivandone il rovesciamento.
Conal

Mentre vedo perché World -> (a, World)è rotto, non sono chiaro sul motivo per cui la sostituzione World -> PowerSet [(a,World)]modella correttamente la concorrenza, ecc. Per me, ciò sembra implicare che i programmi in IOdovrebbero essere eseguiti in qualcosa di simile alla lista monade, applicandosi a ogni elemento del set restituito dalla IOazione. Cosa mi sto perdendo?
Antal Spector-Zabusky

3
@ Absz: Primo, il mio modello suggerito, World -> PowerSet [(a,World)]non è corretto. Proviamo World -> PowerSet ([World],a)invece. PowerSetfornisce l'insieme dei possibili risultati (non determinismo). [World]è sequenze di stati intermedi (non la monade lista / non determinismo), che consente l'interleaving (pianificazione dei thread). E ([World],a)non è neppure del tutto corretto, in quanto consente l'accesso a aprima di attraversare tutti gli stati intermedi. Definisci invece l'uso World -> PowerSet (Computation a)dovedata Computation a = Result a | Step World (Computation a)
Conal

Continuo a non vedere alcun problema con World -> (a, World). Se il Worldtipo include davvero tutto il mondo, allora include anche le informazioni su tutti i processi in esecuzione contemporaneamente, e anche il "seme casuale" di tutto il non determinismo. Il risultato Worldè un mondo con il tempo avanzato e alcune interazioni eseguite. L'unico vero problema con questo modello sembra essere che è troppo generale e i valori di Worldnon possono essere costruiti e manipolati.
Rotsor

17

La programmazione funzionale deriva dal lambda Calculus. Se vuoi veramente capire la programmazione funzionale, dai un'occhiata a http://worrydream.com/AlligatorEggs/

È un modo "divertente" per imparare lambda calcolo e portarti nell'entusiasmante mondo della programmazione funzionale!

In che modo conoscere Lambda Calculus è utile nella programmazione funzionale.

Quindi Lambda Calculus è la base per molti linguaggi di programmazione del mondo reale come Lisp, Scheme, ML, Haskell, ...

Supponiamo di voler descrivere una funzione che aggiunge tre a qualsiasi input per farlo, quindi scriveremmo:

plus3 x = succ(succ(succ x)) 

Leggi "più3 è una funzione che, se applicata a qualsiasi numero x, produce il successore del successore del successore di x"

Nota che la funzione che aggiunge 3 a qualsiasi numero non deve essere chiamata più3; il nome "plus3" è solo una comoda scorciatoia per denominare questa funzione

(plus3 x) (succ 0) ≡ ((λ x. (succ (succ (succ x)))) (succ 0))

Si noti che usiamo il simbolo lambda per una funzione (penso che assomigli un po 'a un alligatore, immagino sia da lì che sia nata l'idea per le uova di alligatore)

Il simbolo lambda è l' alligatore (una funzione) e la x è il suo colore. Puoi anche pensare a x come un argomento (le funzioni Lambda Calculus sono in realtà solo per avere un argomento), il resto puoi pensarlo come il corpo della funzione.

Ora considera l'astrazione:

g  λ f. (f (f (succ 0)))

L'argomento f viene utilizzato in una posizione di funzione (in una chiamata). Chiamiamo ga funzione di ordine superiore perché accetta un'altra funzione come input. Puoi pensare alle altre chiamate di funzione f come " uova ". Ora prendendo le due funzioni o " Alligatori " che abbiamo creato possiamo fare qualcosa del genere:

(g plus3) =  f. (f (f (succ 0)))(λ x . (succ (succ (succ x)))) 
= ((λ x. (succ (succ (succ x)))((λ x. (succ (succ (succ x)))) (succ 0)))
 = ((λ x. (succ (succ (succ x)))) (succ (succ (succ (succ 0)))))
 = (succ (succ (succ (succ (succ (succ (succ 0)))))))

Se noti, puoi vedere che il nostro λ f Alligator mangia il nostro λ x Alligator e poi λ x Alligator e muore. Quindi il nostro λ x Alligator rinasce nelle uova di Alligator di λ f. Quindi il processo si ripete e λ x Alligator a sinistra ora mangia l'altro λ x Alligator a destra.

Quindi puoi usare questo semplice insieme di regole di " Alligatori " che mangiano " Alligatori " per progettare una grammatica e così sono nati i linguaggi di programmazione Funzionale!

Quindi puoi vedere se conosci Lambda Calculus capirai come funzionano i linguaggi funzionali.


@tuckster: ho studiato il lambda calcolo un certo numero di volte prima ... e sì l'articolo di AlligatorEggs ha senso per me. Ma non sono in grado di collegarlo alla programmazione. Per me, in questo momento, il labda calcolo è come una teoria separata, che è proprio lì. Come vengono utilizzati i concetti di lambda calcolo nei linguaggi di programmazione?
Lazer

3
@eSKay: Haskell è lambda calcolo, con un sottile strato di zucchero sintattico per farlo sembrare più simile a un normale linguaggio di programmazione. Le lingue della famiglia Lisp sono anche molto simili al lambda calcolo non tipizzato, che è ciò che rappresenta le uova di alligatore. Lo stesso Lambda Calculus è essenzialmente un linguaggio di programmazione minimalista, un po 'come un "linguaggio assembly di programmazione funzionale".
CA McCann

@ eSKay: ho aggiunto qualcosa su come si relaziona con alcuni esempi. Spero che aiuti!
PJT

Se intendi sottrarre dalla mia risposta, potresti lasciare un commento sul perché così posso provare a migliorare la mia risposta. Grazie.
PJT

14

La tecnica per gestire lo stato in Haskell è molto semplice. E non hai bisogno di capire le monadi per capirlo.

In un linguaggio di programmazione con stato, in genere hai un valore memorizzato da qualche parte, un codice viene eseguito e quindi hai un nuovo valore memorizzato. Nelle lingue imperative questo stato è solo da qualche parte "sullo sfondo". In un linguaggio funzionale (puro) lo rendi esplicito, quindi scrivi esplicitamente la funzione che trasforma lo stato.

Quindi, invece di avere uno stato di tipo X, scrivi funzioni che mappano X su X. Questo è tutto! Passi dal pensare allo stato al pensare a quali operazioni vuoi eseguire sullo stato. È quindi possibile concatenare queste funzioni insieme e combinarle insieme in vari modi per creare interi programmi. Ovviamente non sei limitato a mappare solo X in X. Puoi scrivere funzioni per prendere varie combinazioni di dati come input e restituire varie combinazioni alla fine.

Le monadi sono uno strumento, tra i tanti, per aiutare a organizzare questo. Ma le monadi in realtà non sono la soluzione al problema. La soluzione è pensare alle trasformazioni di stato invece che allo stato.

Funziona anche con I / O. In effetti, ciò che accade è questo: invece di ricevere input dall'utente con un equivalente diretto di scanfe memorizzarlo da qualche parte, scrivi invece una funzione per dire cosa faresti con il risultato scanfse lo avessi, e poi lo passi funzione all'API di I / O. Questo è esattamente quello che >>=fa quando usi la IOmonade in Haskell. Quindi non è mai necessario memorizzare il risultato di qualsiasi I / O ovunque: è sufficiente scrivere codice che indichi come si desidera trasformarlo.


8

(Alcuni linguaggi funzionali consentono funzioni impure.)

Per linguaggi puramente funzionali , l'interazione del mondo reale è solitamente inclusa come uno degli argomenti della funzione, in questo modo:

RealWorld pureScanf(RealWorld world, const char* format, ...);

Linguaggi diversi hanno strategie diverse per astrarre il mondo dal programmatore. Haskell, ad esempio, usa le monadi per nascondere l' worldargomento.


Ma la parte pura del linguaggio funzionale stesso è già completa di Turing, il che significa che qualsiasi cosa fattibile in C è fattibile anche in Haskell. La principale differenza rispetto al linguaggio imperativo è invece di modificare gli stati in atto:

int compute_sum_of_squares (int min, int max) {
  int result = 0;
  for (int i = min; i < max; ++ i)
     result += i * i;  // modify "result" in place
  return result;
}

Incorporate la parte di modifica in una chiamata di funzione, di solito trasformando i cicli in ricorsioni:

int compute_sum_of_squares (int min, int max) {
  if (min >= max)
    return 0;
  else
    return min * min + compute_sum_of_squares(min + 1, max);
}

O semplicemente computeSumOfSquares min max = sum [x*x | x <- [min..max]];-)
fredoverflow

@Fred: la comprensione della lista è solo uno zucchero sintattico (e quindi devi spiegare in dettaglio la monade della lista). E come si implementa sum? La ricorsione è ancora necessaria.
kennytm

3

Il linguaggio funzionale può salvare lo stato! Di solito ti incoraggiano o ti costringono a essere esplicito nel farlo.

Ad esempio, dai un'occhiata a State Monad di Haskell .


9
E tieni presente che non c'è nulla su Stateo Monadche abilita lo stato, poiché entrambi sono definiti in termini di strumenti semplici, generali e funzionali. Catturano solo modelli rilevanti, quindi non devi reinventare così tanto la ruota.
Conal


1

haskell:

main = do no <- readLn
          print (no + 1)

Ovviamente puoi assegnare cose alle variabili nei linguaggi funzionali. Non puoi cambiarle (quindi praticamente tutte le variabili sono costanti nei linguaggi funzionali).


@ sepp2k: perché, che male c'è a cambiarli?
Lazer

@eSKay se non puoi cambiare le variabili allora sai che sono sempre le stesse. Questo rende più facile il debug, costringe a creare funzioni più semplici che fanno una cosa sola e molto bene. Aiuta anche molto quando si lavora con la concorrenza.
Henrik Hansen

9
@eSKay: I programmatori funzionali credono che lo stato mutabile introduca molte possibilità di bug e renda più difficile ragionare sul comportamento dei programmi. Ad esempio, se hai una chiamata di funzione f(x)e vuoi vedere qual è il valore di x, devi solo andare nel punto in cui è definito x. Se x fosse mutabile dovresti anche considerare se c'è un punto in cui x potrebbe essere cambiato tra la sua definizione e il suo utilizzo (che non è banale se x non è una variabile locale).
sepp2k

6
Non sono solo i programmatori funzionali a diffidare dello stato mutevole e degli effetti collaterali. Gli oggetti immutabili e la separazione comando / query sono ben considerati da parecchi programmatori OO, e quasi tutti pensano che le variabili globali mutabili siano una cattiva idea. Lingue come Haskell
CA McCann

5
@eSKay: Non è tanto che la mutazione sia dannosa, è che se accetti di evitare la mutazione diventa molto più facile scrivere codice modulare e riutilizzabile. Senza uno stato mutabile condiviso, l'accoppiamento tra le diverse parti del codice diventa esplicito ed è molto più facile comprendere e mantenere il progetto. John Hughes lo spiega meglio di me; prendi il suo articolo Why Functional Programming Matters .
Norman Ramsey
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.