Come puoi fare qualcosa di utile senza uno stato mutabile?

Ho letto un sacco di cose sulla programmazione funzionale ultimamente, e posso capirne la maggior parte, ma l’unica cosa che non riesco a capire è la codifica senza stato. Mi sembra che semplificare la programmazione rimuovendo lo stato mutabile sia come “semplificare” un’auto rimuovendo il cruscotto: il prodotto finito può essere più semplice, ma buona fortuna farlo interagire con gli utenti finali.

Quasi tutte le applicazioni utente a cui posso pensare implicano lo stato come un concetto centrale. Se scrivi un documento (o un post SO), lo stato cambia ad ogni nuovo input. Oppure, se giochi a un videogioco, ci sono tonnellate di variabili di stato, a cominciare dalle posizioni di tutti i personaggi, che tendono a muoversi costantemente. Come puoi fare qualcosa di utile senza tenere traccia dei cambiamenti dei valori?

Ogni volta che trovo qualcosa che discute di questo problema, è scritto in modo molto tecnico-funzionale, il che presuppone un pesante background FP che non ho. Qualcuno conosce un modo per spiegare questo a qualcuno con una buona, solida comprensione della codifica imperativa, ma chi è un n00b completo sul lato funzionale?

EDIT: Un sacco di risposte finora sembrano cercare di convincermi dei vantaggi dei valori immutabili. Ho capito quella parte. Ha perfettamente senso. Quello che non capisco è come tenere traccia dei valori che devono cambiare e cambiarsi costantemente, senza variabili mutabili.

    Oppure, se giochi a un videogioco, ci sono tonnellate di variabili di stato, a cominciare dalle posizioni di tutti i personaggi, che tendono a muoversi costantemente. Come puoi fare qualcosa di utile senza tenere traccia dei cambiamenti dei valori?

    Se sei interessato, ecco una serie di articoli che descrivono la programmazione del gioco con Erlang.

    Probabilmente questa risposta non ti piacerà, ma non riceverai un programma funzionale finché non lo userai. Posso postare esempi di codice e dire “Qui, non vedi “, ma se non capisci la syntax e i principi di base, i tuoi occhi si spalancano. Dal tuo punto di vista, sembra che io stia facendo la stessa cosa di un linguaggio imperativo, ma solo impostando tutti i tipi di limiti per rendere la programmazione più difficile. Il mio punto di vista, stai vivendo il paradosso di Blub .

    All’inizio ero scettico, ma ho saltato sul treno di programmazione funzionale alcuni anni fa e me ne sono innamorato. Il trucco con la programmazione funzionale è essere in grado di riconoscere schemi, assegnazioni di variabili particolari e spostare lo stato imperativo in pila. Un ciclo for, ad esempio, diventa ricorsione:

    // Imperative let printTo x = for a in 1 .. x do printfn "%i" a // Recursive let printTo x = let rec loop a = if a <= x then printfn "%i" a; loop (a + 1) loop 1 

    Non è molto carina, ma abbiamo ottenuto lo stesso effetto senza alcuna mutazione. Ovviamente, laddove ansible, ci piace evitare di fare il ciclo del tutto e basta astrarlo via:

     // Preferred let printTo x = seq { 1 .. x } |> Seq.iter (fun a -> printfn "%i" a) 

    Il metodo Seq.iter enumererà attraverso la raccolta e invocherà la funzione anonima per ciascun elemento. Molto maneggevole 🙂

    Lo so, stampare numeri non è esattamente impressionante. Tuttavia, possiamo utilizzare lo stesso approccio con i giochi: mantenere tutti gli stati nello stack e creare un nuovo object con le nostre modifiche nella chiamata ricorsiva. In questo modo, ogni fotogramma è un'istantanea stateless del gioco, in cui ogni frame crea semplicemente un object nuovo di zecca con le modifiche desiderate di qualsiasi object stateless necessiti di aggiornamento. Lo pseudocodice per questo potrebbe essere:

     // imperative version pacman = new pacman(0, 0) while true if key = UP then pacman.y++ elif key = DOWN then pacman.y-- elif key = LEFT then pacman.x-- elif key = UP then pacman.x++ render(pacman) // functional version let rec loop pacman = render(pacman) let x, y = switch(key) case LEFT: pacman.x - 1, pacman.y case RIGHT: pacman.x + 1, pacman.y case UP: pacman.x, pacman.y - 1 case DOWN: pacman.x, pacman.y + 1 loop(new pacman(x, y)) 

    Le versioni imperativa e funzionale sono identiche, ma la versione funzionale chiaramente non usa uno stato mutabile. Il codice funzionale mantiene tutto lo stato in pila: la cosa bella di questo approccio è che, se qualcosa va storto, il debugging è facile, tutto ciò che serve è una traccia stack.

    Questo scala fino a qualsiasi numero di oggetti nel gioco, perché tutti gli oggetti (o le raccolte di oggetti correlati) possono essere visualizzati nella loro stessa discussione.

    Quasi tutte le applicazioni utente a cui posso pensare implicano lo stato come un concetto centrale.

    Nei linguaggi funzionali, piuttosto che mutare lo stato degli oggetti, restituiamo semplicemente un nuovo object con le modifiche che vogliamo. È più efficiente di quanto sembri. Le strutture dati, ad esempio, sono molto facili da rappresentare come strutture di dati immutabili. Le pile, ad esempio, sono notoriamente facili da implementare:

     using System; namespace ConsoleApplication1 { static class Stack { public static Stack Cons(T hd, Stack tl) { return new Stack(hd, tl); } public static Stack Append(Stack x, Stack y) { return x == null ? y : Cons(x.Head, Append(x.Tail, y)); } public static void Iter(Stack x, Action f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } } } class Stack { public readonly T Head; public readonly Stack Tail; public Stack(T hd, Stack tl) { this.Head = hd; this.Tail = tl; } } class Program { static void Main(string[] args) { Stack x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null)))); Stack y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null)))); Stack z = Stack.Append(x, y); Stack.Iter(z, a => Console.WriteLine(a)); Console.ReadKey(true); } } } 

    Il codice sopra costruisce due elenchi immutabili, li accoda insieme per creare una nuova lista e aggiunge i risultati. Nessuno stato mutabile viene utilizzato in qualsiasi punto dell'applicazione. Sembra un po 'ingombrante, ma è solo perché C # è un linguaggio prolisso. Ecco il programma equivalente in F #:

     type 'a stack = | Cons of 'a * 'a stack | Nil let rec append xy = match x with | Cons(hd, tl) -> Cons(hd, append tl y) | Nil -> y let rec iter f = function | Cons(hd, tl) -> f(hd); iter f tl | Nil -> () let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil)))) let y = Cons(5, Cons(6, Cons(7, Cons(8, Nil)))) let z = append xy iter (fun a -> printfn "%i" a) z 

    Non è necessario alcun mutevole per creare e manipolare le liste. Quasi tutte le strutture di dati possono essere facilmente convertite nei loro equivalenti funzionali. Ho scritto qui una pagina che fornisce implementazioni immutabili di pile, code, mucchi di sinistra, alberi rosso-neri, elenchi pigri. Non un singolo snippet di codice contiene uno stato mutabile. Per "mutare" un albero, ne creo uno nuovo di zecca con il nuovo nodo che voglio - questo è molto efficiente perché non ho bisogno di fare una copia di ogni nodo nell'albero, posso riutilizzare i vecchi nel mio nuovo albero.

    Usando un esempio più significativo, ho anche scritto questo parser SQL che è totalmente privo di stato (o almeno il mio codice è senza stato, non so se la libreria di lexing sottostante è senza stato).

    La programmazione stateless è tanto espressiva e potente quanto una programmazione stateful, richiede solo un po 'di pratica per allenarsi a iniziare a pensare senza età. Certamente, "programmazione stateless quando ansible, programmazione con stati dove necessario" sembra essere il motto della maggior parte dei linguaggi funzionali impuri. Non c'è nulla di male nel ricorrere ai mutabili quando l'approccio funzionale non è altrettanto pulito o efficiente.

    Risposta breve: non puoi.

    Allora, qual è il problema dell’immutabilità?

    Se sei esperto in una lingua imperativa, allora sai che le “globali sono cattive”. Perché? Perché introducono (o hanno il potenziale per introdurre) alcune dipendenze molto difficili da districare nel codice. E le dipendenze non sono buone; vuoi che il tuo codice sia modulare . Parti del programma non influenzano le altre parti il ​​meno ansible. E FP ti porta al Santo Graal della modularità: nessun effetto collaterale. Hai appena il tuo f (x) = y. Metti x dentro, tirati fuori. Nessuna modifica a x o qualsiasi altra cosa. FP ti fa smettere di pensare allo stato e iniziare a pensare in termini di valori. Tutte le tue funzioni ricevono semplicemente valori e producono nuovi valori.

    Questo ha diversi vantaggi.

    Prima di tutto, nessun effetto collaterale significa programmi più semplici, più facili da ragionare. Non c’è da preoccuparsi che l’introduzione di una nuova parte del programma interferisca e blocchi una parte esistente e funzionante.

    In secondo luogo, ciò rende banalmente parallelo il programma (la parallelizzazione efficiente è un’altra questione).

    In terzo luogo, ci sono alcuni possibili vantaggi in termini di prestazioni. Di ‘che hai una funzione:

     double x = 2 * x 

    Ora inserisci un valore di 3 in e ottieni un valore di 6. Ogni volta. Ma puoi farlo anche in modo imperativo, giusto? Sì. Ma il problema è che in caso di necessità, puoi fare ancora di più . Posso fare:

     int y = 2; int double(x){ return x * y; } 

    ma potrei anche farlo

     int y = 2; int double(x){ return x * (y++); } 

    Il compilatore imperativo non sa se avrò o meno effetti collaterali, il che rende più difficile l’ottimizzazione (ad esempio il doppio 2 non deve essere 4 ogni volta). Il funzionale sa che non lo farò – quindi, può ottimizzare ogni volta che vede “doppio 2”.

    Ora, anche se creare nuovi valori ogni volta sembra incredibilmente dispendioso per tipi di valori complessi in termini di memoria del computer, non deve essere così. Perché se hai f (x) = y, ei valori xey sono “per lo più uguali” (es. Alberi che differiscono solo in alcune foglie) allora x e y possono condividere parti di memoria – perché nessuno di loro muterà .

    Quindi se questa cosa immutabile è così grande, perché ho risposto che non puoi fare nulla di utile senza uno stato mutevole. Bene, senza mutabilità, il tuo intero programma sarebbe una gigantesca funzione f (x) = y. E lo stesso vale per tutte le parti del programma: solo funzioni e funzioni in senso “puro”. Come ho detto, questo significa f (x) = y ogni volta. Ad esempio, readFile (“myFile.txt”) dovrebbe restituire sempre lo stesso valore di stringa. Non troppo utile.

    Pertanto, ogni FP fornisce alcuni mezzi di stato mutante. I linguaggi funzionali “puri” (ad es. Haskell) lo fanno usando concetti alquanto spaventosi come le monadi, mentre quelli “impuri” (es. ML) lo consentono direttamente.

    E, naturalmente, i linguaggi funzionali sono dotati di una serie di altri gadget che rendono la programmazione più efficiente, come le funzioni di prima class, ecc.

    Si noti che dire che la programmazione funzionale non ha “stato” è un po ‘fuorviante e potrebbe essere la causa della confusione. Non ha sicuramente uno “stato mutabile”, ma può ancora avere valori che vengono manipolati; semplicemente non possono essere modificati sul posto (ad es. devi creare nuovi valori dai vecchi valori).

    Questa è una grossolana semplificazione, ma immaginate di avere un linguaggio OO, in cui tutte le proprietà sulle classi sono impostate una sola volta nel costruttore, tutti i metodi sono funzioni statiche. Puoi ancora eseguire praticamente qualsiasi calcolo avendo i metodi prendere oggetti contenenti tutti i valori di cui hanno bisogno per i loro calcoli e quindi restituire nuovi oggetti con il risultato (magari anche una nuova istanza dello stesso object).

    Potrebbe essere ‘difficile’ tradurre il codice esistente in questo paradigma, ma è perché richiede davvero un modo completamente diverso di pensare al codice. Come effetto collaterale però nella maggior parte dei casi hai molte opportunità per il parallelismo gratis.

    Addendum: (Per quanto riguarda la modifica di come tenere traccia dei valori che devono essere modificati)
    Verrebbero archiviati in una struttura dati immutabile, naturalmente …

    Questa non è una “soluzione” suggerita, ma il modo più semplice per vedere che funzionerà sempre è che è ansible memorizzare questi valori immutabili in una struttura di tipo map / hashtable, definita da un “nome variabile”.

    Ovviamente nelle soluzioni pratiche dovresti usare un approccio più sano, ma questo dimostra che nel peggiore dei casi, se non altro funzionasse, potresti “simulare” lo stato mutabile con una tale mappa che ti porti dietro attraverso l’albero delle invocazioni.

    Penso che ci sia un leggero fraintendimento. I programmi funzionali puri hanno stato. La differenza è come questo stato è modellato. Nella pura programmazione funzionale, lo stato è manipolato da funzioni che prendono uno stato e restituiscono lo stato successivo. Il sequenziamento attraverso gli stati viene quindi ottenuto passando lo stato attraverso una sequenza di funzioni pure.

    Anche lo stato globale mutabile può essere modellato in questo modo. Ad Haskell, ad esempio, un programma è una funzione da un mondo a un mondo. Cioè, tu passi per l’intero universo e il programma restituisce un nuovo universo. In pratica, però, devi solo passare nelle parti dell’universo in cui il tuo programma è effettivamente interessato. E i programmi restituiscono effettivamente una sequenza di azioni che fungono da istruzioni per l’ambiente operativo in cui viene eseguito il programma.

    Volevi vederlo spiegato in termini di programmazione imperativa. OK, diamo un’occhiata ad una programmazione imperativa davvero semplice in un linguaggio funzionale.

    Considera questo codice:

     int x = 1; int y = x + 1; x = x + y; return x; 

    Bel codice imperativo standard di bog. Non fa nulla di interessante, ma va bene per l’illustrazione. Penso che sarete d’accordo sul fatto che lo stato sia coinvolto qui. Il valore della variabile x cambia nel tempo. Ora, cambiamo leggermente la notazione inventando una nuova syntax:

     let x = 1 in let y = x + 1 in let z = x + y in z 

    Metti le parentesi per rendere più chiaro cosa significa:

     let x = 1 in (let y = x + 1 in (let z = x + y in (z))) 

    Quindi vedi, lo stato è modellato da una sequenza di espressioni pure che legano le variabili libere delle seguenti espressioni.

    Scoprirai che questo modello può modellare qualsiasi tipo di stato, anche IO.

    Ecco come si scrive codice senza stato mutabile : invece di mettere lo stato mutevole in variabili mutabili, lo si inserisce nei parametri delle funzioni. E invece di scrivere loop, scrivi funzioni ricorsive. Quindi per esempio questo codice imperativo:

     f_imperative(y) { local x; x := e; while p(x, y) do x := g(x, y) return h(x, y) } 

    diventa questo codice funzionale (syntax Scheme-like):

     (define (f-functional y) (letrec ( (f-helper (lambda (xy) (if (pxy) (f-helper (gxy) y) (hxy))))) (f-helper ey))) 

    o questo codice Haskellish

     f_fun y = h x_final y where x_initial = e x_final = loop x_initial loop x = if pxy then loop (gxy) else x 

    Per quanto riguarda il motivo per cui ai programmatori funzionali piace fare questo (cosa che non hai chiesto), più pezzi del tuo programma sono senza stato, più modi ci sono per mettere insieme i pezzi senza avere nulla da rompere . Il potere del paradigma stateless non sta nell’essere apolidi (o nella purezza) di per sé , ma nella capacità che ti dà di scrivere funzioni potenti, riutilizzabili e combinarle.

    Puoi trovare un buon tutorial con molti esempi nel documento di John Hughes. Perché la programmazione funzionale è importante .

    Sono solo diversi modi di fare la stessa cosa.

    Considera un semplice esempio come aggiungere i numeri 3, 5 e 10. Immagina di pensare a farlo modificando prima il valore di 3 aggiungendo 5 a esso, quindi aggiungendo 10 a quel “3”, quindi emettendo il valore corrente di ” 3 “(18). Ciò sembra palesemente ridicolo, ma è in sostanza il modo in cui spesso viene eseguita la programmazione imperativa basata sullo stato. In effetti, puoi avere molti “3” diversi che hanno il valore 3, eppure sono diversi. Tutto ciò sembra strano, perché siamo stati così radicati nell’idea, assolutamente enormemente ragionevole, che i numeri sono immutabili.

    Ora pensate di aggiungere 3, 5 e 10 quando prendete i valori per essere immutabili. Aggiungi 3 e 5 per produrre un altro valore, 8, quindi aggiungi 10 a quel valore per produrre ancora un altro valore, 18.

    Questi sono modi equivalenti per fare la stessa cosa. Tutte le informazioni necessarie esistono in entrambi i metodi, ma in forms diverse. In uno l’informazione esiste come stato e nelle regole per cambiare stato. Nell’altra l’informazione esiste in dati immutabili e definizioni funzionali.

    La programmazione funzionale evita lo stato e enfatizza la funzionalità. Non c’è mai niente come nessuno stato, anche se lo stato potrebbe effettivamente essere qualcosa che è immutabile o infuso nell’architettura di ciò con cui stai lavorando. Considera la differenza tra un server web statico che carica solo i file dal filesystem rispetto a un programma che implementa un cubo di Rubik. Il primo verrà implementato in termini di funzioni progettate per trasformare una richiesta in una richiesta di percorso file in una risposta dal contenuto di quel file. Praticamente nessuno stato è necessario al di là di un piccolo bit di configurazione (lo “stato” del file system è davvero al di fuori dell’ambito del programma. Il programma funziona allo stesso modo indipendentemente dallo stato in cui si trovano i file). In quest’ultimo caso, è necessario modellare il cubo e l’implementazione del programma su come le operazioni su quel cubo cambiano il suo stato.

    In effetti è abbastanza facile avere qualcosa che assomiglia a uno stato mutevole anche in lingue senza stato mutabile.

    Considera una funzione con tipo s -> (a, s) . Traducendo dalla syntax di Haskell, significa una funzione che accetta un parametro di tipo ” s ” e restituisce una coppia di valori, di tipi ” a ” e ” s “. Se s è il tipo del nostro stato, questa funzione accetta uno stato e restituisce un nuovo stato, e possibilmente un valore (puoi sempre restituire “unit” aka () , che è una specie di equivalente a ” void ” in C / C ++, come il tipo ” a “). Se si concatenano più chiamate di funzioni con tipi come questo (ottenendo lo stato restituito da una funzione e passandola alla successiva), si ha uno stato “mutabile” (in realtà ci si trova in ogni funzione che crea un nuovo stato e si abbandona quello precedente ).

    Potrebbe essere più facile capire se si immagina lo stato mutabile come lo “spazio” in cui si sta eseguendo il programma, e quindi si pensi alla dimensione temporale. All’istante t1, lo “spazio” si trova in una determinata condizione (ad esempio, qualche posizione di memoria ha il valore 5). In un istante successivo t2, si trova in una condizione diversa (ad esempio, la posizione di memoria ha ora il valore 10). Ognuna di queste “fette” di tempo è uno stato ed è immutabile (non puoi tornare indietro nel tempo per cambiarle). Quindi, da questo punto di vista, sei passato dallo spaziotempo completo a una freccia temporale (il tuo stato mutabile) a un insieme di fette di spaziotempo (diversi stati immutabili), e il tuo programma tratta semplicemente ogni fetta come valore e calcola ciascuna di loro come una funzione applicata alla precedente.

    OK, forse non era più facile da capire 🙂

    Potrebbe sembrare inefficiente rappresentare esplicitamente l’intero stato del programma come un valore, che deve essere creato solo per essere scartato l’istante successivo (subito dopo la creazione di uno nuovo). Per alcuni algoritmi potrebbe essere naturale, ma quando non lo è, c’è un altro trucco. Invece di uno stato reale, puoi usare uno stato falso che non è altro che un indicatore (chiamiamo il tipo di questo stato di stato falso State# ). Questo stato falso esiste dal punto di vista della lingua e viene passato come qualsiasi altro valore, ma il compilatore lo omette completamente durante la generazione del codice macchina. Serve solo a segnare la sequenza di esecuzione.

    Ad esempio, supponiamo che il compilatore ci fornisca le seguenti funzioni:

     readRef :: Ref a -> State# -> (a, State#) writeRef :: Ref a -> a -> State# -> (a, State#) 

    Traducendo da queste dichiarazioni tipo Haskell, readRef riceve qualcosa che assomiglia a un puntatore o un handle a un valore di tipo ” a ” e allo stato falso e restituisce il valore di tipo ” a ” puntato dal primo parametro e un nuovo stato falso writeRef è simile, ma cambia invece il valore puntato.

    Se chiami readRef e poi lo passi, lo stato falso restituito da writeRef (forse con altre chiamate a funzioni non correlate nel mezzo, questi valori di stato creano una “catena” di chiamate di funzione), restituirà il valore scritto. Puoi chiamare writeRef nuovo con lo stesso puntatore / handle e scriverà nella stessa locazione di memoria – ma, dal momento che concettualmente sta restituendo uno stato nuovo (falso), lo stato (falso) è ancora imutabile (ne è stato creato uno nuovo) creato”). Il compilatore chiamerà le funzioni nell’ordine in cui dovrebbe chiamarle se ci fosse una variabile di stato reale che doveva essere calcasting, ma l’unico stato che c’è è lo stato completo (mutevole) dell’hardware reale.

    (Coloro che sanno che Haskell noterà che ho semplificato molto le cose e ho omesso molti dettagli importanti.Per chi vuole vedere più dettagli, dai un’occhiata a Control.Monad.State dalla mtl e alla ST s e IO (aka ST RealWorld ).)

    Potresti chiederti perché farlo in un modo così indiretto (invece di avere semplicemente uno stato mutabile nella lingua). Il vero vantaggio è che hai reificato lo stato del tuo programma. Quello che prima era implicito (il tuo stato del programma era globale, tenendo conto di cose come l’ azione a distanza ) è ora esplicito. Le funzioni che non ricevono e restituiscono lo stato non possono modificarlo o essere influenzato da esso; sono “puri”. Ancora meglio, puoi avere thread di stato separati, e con un po ‘di magia del tipo, possono essere usati per incorporare un calcolo imperativo all’interno di uno puro, senza renderlo impuro (la monade di ST in Haskell è quella normalmente usata per questo trucco lo State# cui sopra è in effetti State# s GHC, utilizzato dalla sua implementazione delle monadi ST e IO ).

    Sono in ritardo con la discussione, ma volevo aggiungere alcuni punti per le persone che hanno difficoltà con la programmazione funzionale.

    1. I linguaggi funzionali mantengono gli stessi aggiornamenti di stato dei linguaggi imperativi, ma lo fanno passando lo stato aggiornato alle chiamate di funzione successive . Ecco un esempio molto semplice di percorrere una linea numerica. Il tuo stato è la tua posizione attuale.

    Prima il modo imperativo (in pseudocodice)

     moveTo(dest, cur): while (cur != dest): if (cur < dest): cur += 1 else: cur -= 1 return cur 

    Ora il modo funzionale (in pseudocodice). Mi sto appoggiando pesantemente all'operatore ternario perché voglio che le persone provenienti da sfondi imperativi siano effettivamente in grado di leggere questo codice. Quindi se non usi molto l'operatore ternario (l'ho sempre evitato nei miei giorni imperiosi) ecco come funziona.

     predicate ? if-true-expression : if-false-expression 

    Puoi concatenare l'espressione ternaria mettendo una nuova espressione ternaria al posto della falsa espressione

     predicate1 ? if-true1-expression : predicate2 ? if-true2-expression : else-expression 

    Quindi con questo in mente, ecco la versione funzionale.

     moveTo(dest, cur): return ( cur == dest ? return cur : cur < dest ? moveTo(dest, cur + 1) : moveTo(dest, cur - 1) ) 

    Questo è un esempio banale. Se questo spostasse le persone in un mondo di gioco, dovresti introdurre effetti collaterali come disegnare la posizione corrente dell'object sullo schermo e introdurre un po 'di ritardo in ogni chiamata in base alla velocità con cui l'object si muove. Ma non avresti ancora bisogno di uno stato mutabile.

    1. La lezione è che lo stato "mutato" delle lingue funzionali chiama la funzione con parametri diversi. Ovviamente questo non muta realmente nessuna variabile, ma è così che si ottiene un effetto simile. Questo significa che dovrai abituarti a pensare in modo ricorsivo se vuoi fare una programmazione funzionale.

    2. Imparare a pensare in modo ricorsivo non è difficile, ma richiede sia pratica che un kit di strumenti. Quella piccola sezione in quel libro "Impara Java" dove hanno usato la ricorsione per calcolare fattoriale non la taglia. È necessario un kit di strumenti come la creazione di processi iterativi dalla ricorsione (questo è il motivo per cui la ricorsione in coda è essenziale per il linguaggio funzionale), continuazioni, invarianti, ecc. Non si farebbe la programmazione OO senza conoscere modificatori di accesso, interfacce, ecc. per la programmazione funzionale.

    La mia raccomandazione è di fare Little Schemer (nota che io dico "do" e non "read") e poi fare tutti gli esercizi in SICP. Quando hai finito, avrai un cervello diverso rispetto a quando hai iniziato.

    Oltre alle grandi risposte che gli altri stanno dando, pensate alle classi Integer e String in Java. Le istanze di queste classi sono immutabili, ma ciò non rende le classi inutili solo perché le loro istanze non possono essere modificate. L’immutabilità ti dà un po ‘di sicurezza. Sapete se utilizzate un’istanza String o Integer come chiave per una Map , la chiave non può essere cambiata. Confrontalo con la class Date in Java:

     Date date = new Date(); mymap.put(date, date.toString()); // Some time later: date.setTime(new Date().getTime()); 

    Hai cambiato silenziosamente una chiave nella tua mappa! Lavorare con oggetti immutabili, come nella Programmazione funzionale, è molto più pulito. È più facile ragionare su quali effetti collaterali si verificano – nessuno! Ciò significa che è più semplice per il programmatore e anche più semplice per l’ottimizzatore.

    Usando un po ‘di creatività e pattern matching, sono stati creati giochi stateless:

    • CSSPlay: gioco del labirinto
    • CSSPlay: Maze Game 2
    • CSSPlay: Tic-Tac-Toe
    • Pure CSS Tic-Tac-Toe
    • CSSPlay: Pong
    • CSSPlay: Ping-Pong
    • CSSPlay: poliziotti e ladri
    • CSSPlay: Whack-a-Rat
    • CSS3 Pong: Insane Cose da fare con i CSS

    così come dimostrazioni a rotazione:

    • CSSPlay: Random Heroes
    • Orologio SVG analogico animato
    • Pendolo SVG animato
    • Racer SVG animati
    • CSS3: fare neve

    e visualizzazioni:

    • XSLT Mandlebrot

    Questo è il modo in cui FORTRAN funziona senza blocchi COMMON: scriverei i metodi con i valori passati e le variabili locali. Questo è tutto.

    La programmazione orientata agli oggetti ci ha portato lo stato e il comportamento insieme, ma è stata una nuova idea quando l’ho incontrata per la prima volta da C ++ nel 1994.

    Accidenti, ero un programmatore funzionante quando ero un ingegnere meccanico e non lo sapevo!

    Per applicazioni altamente interattive come i giochi, Functional Reactive Programming è tuo amico: se puoi formulare le proprietà del mondo del tuo gioco come valori variabili nel tempo (e / o flussi di eventi), sei pronto! Queste formule saranno a volte anche più naturali e rivelatrici di uno stato, ad esempio per una palla in movimento, è ansible utilizzare direttamente la nota legge x = v * t . E la cosa migliore è che le regole del gioco scritte in questo modo sono meglio delle astrazioni orientate agli oggetti. Per esempio, in questo caso, la velocità della palla può essere anche un valore variabile nel tempo, che dipende dal stream di eventi che consiste nelle collisioni della palla. Per ulteriori considerazioni concettuali sul design, vedi Making Games in Elm .

    Bear in mind: functional languages are Turing complete. Therefore, any useful task you would perform in an imperitive language can be done in a functional language. At the end of the day though, I think there’s something to be said of a hybrid approach. Languages like F# and Clojure (and I’m sure others) encourage stateless design, but allow for mutability when necessary.

    You can’t have a pure functional language that is useful. There will always be a level of mutability that you have to deal with, IO is one example.

    Think of functional languages as just another tool that you use. Its good for certain things, but not others. The game example you gave might not be the best way to use a functional language, at least the screen will have a mutable state that you can’t do anything about with FP. The way you think of problem and the type of problems you solve with FP will be different from ones you are used to with imperative programming.

    By using lots of recursion.

    Tic Tac Toe in F# (A functional language.)

    Questo è molto semplice. You can use as many variables as you want in functional programming…but only if they’re local variables (contained inside functions). So just wrap your code in functions, pass values back and forth among those functions (as passed parameters and returned values)…and that’s all there is to it!

    Ecco un esempio:

     function ReadDataFromKeyboard() { $input_values = $_POST[]; return $input_values; } function ProcessInformation($input_values) { if ($input_values['a'] > 10) return ($input_values['a'] + $input_values['b'] + 3); else if ($input_values['a'] > 5) return ($input_values['b'] * 3); else return ($input_values['b'] - $input_values['a'] - 7); } function DisplayToPage($data) { print "Based your input, the answer is: "; print $data; print "\n"; } /* begin: */ DisplayToPage ( ProcessInformation ( GetDataFromKeyboard() ) );