Come puoi fare qualcosa di utile senza uno stato mutabile?


265

Ultimamente ho letto molte cose sulla programmazione funzionale e posso capirne la maggior parte, ma l'unica cosa che non riesco proprio a capire è la programmazione senza stato. Mi sembra che semplificare la programmazione rimuovendo lo stato mutevole sia come "semplificare" un'auto rimuovendo il cruscotto: il prodotto finito può essere più semplice, ma buona fortuna facendolo interagire con gli utenti finali.

Quasi ogni applicazione utente che mi viene in mente coinvolge lo stato come un concetto chiave. Se si scrive un documento (o un post SO), lo stato cambia ad ogni nuovo input. O 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 eventualmente fare qualcosa di utile senza tenere traccia della modifica dei valori?

Ogni volta che trovo qualcosa che discute di questo problema, è scritto in funzioni veramente tecniche che presuppongono un background FP pesante che non ho. Qualcuno conosce un modo per spiegare questo a qualcuno con una buona e solida comprensione della codifica imperativa ma chi è un n00b completo dal punto di vista funzionale?

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



1
La mia modesta opinione personale è che è come forza e denaro. Si applica la legge dei rendimenti decrescenti. Se sei abbastanza forte, potrebbero esserci pochi incentivi per diventare leggermente più forti, ma non fa male lavorarci (e alcune persone lo fanno con passione). Lo stesso vale per lo stato mutabile globale. È mia preferenza personale accettare che man mano che la mia abilità di programmazione progredisce, è bene limitare la quantità di stato mutabile globale nel mio codice. Potrebbe non essere mai perfetto, ma è bene lavorare per minimizzare lo stato mutabile globale.
AturSams,

Come nel caso del denaro, verrà raggiunto un punto in cui investirai più tempo, non sarà più estremamente utile e altre priorità saliranno in cima. Se, ad esempio, raggiungi la massima forza possibile (secondo la mia metafora), potrebbe non servire a nessuno scopo utile e potrebbe persino diventare un peso. Ma è bene ancora perseguire l'obiettivo forse irraggiungibile e investire risorse moderate in esso.
AturSams,

7
In breve, in FP, le funzioni non modificano mai lo stato. Alla fine restituiranno qualcosa che sostituisce lo stato corrente. Ma lo stato non viene mai modificato (mutato) sul posto.
jinglesthula,

Ci sono modi per ottenere lo stato di stato senza mutazione (usando la pila da quello che ho capito), ma questa domanda è in un certo senso accanto al punto (anche se è grandiosa). Difficile parlare in modo succinto, ma ecco un post che si spera risponda alla tua domanda medium.com/@jbmilgrom/… . Il TLDR è che la semantica anche di un programma funzionale con stato è immutabile, tuttavia vengono gestite le comunicazioni b / n della funzione del programma.
jbmilgrom,

Risposte:


166

O 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 eventualmente fare qualcosa di utile senza tenere traccia della modifica 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 otterrai un programma funzionale fino a quando non lo utilizzerai. Posso inviare esempi di codice e dire "Qui, non vedi ", ma se non capisci la sintassi e i principi sottostanti, i tuoi occhi si fermano. Dal tuo punto di vista, sembra che stia facendo la stessa cosa di un linguaggio imperativo, ma sto solo impostando tutti i tipi di confini per rendere intenzionalmente più difficile la programmazione. Il mio punto di vista, stai solo vivendo il paradosso di Blub .

All'inizio ero scettico, ma qualche anno fa sono saltato sul treno di programmazione funzionale e me ne sono innamorato. Il trucco con la programmazione funzionale è riuscire a riconoscere schemi, particolari assegnazioni di variabili e spostare lo stato imperativo nello stack. Un for-loop, 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 carino, ma abbiamo ottenuto lo stesso effetto senza mutazione. Ovviamente, laddove possibile, ci piace evitare del tutto il looping e semplicemente astrarlo via:

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

Il metodo Seq.iter enumera la raccolta e invoca la funzione anonima per ogni articolo. Molto maneggevole :)

Lo so, la stampa dei numeri non è esattamente impressionante. Tuttavia, possiamo usare lo stesso approccio con i giochi: tenere tutto lo stato nello stack e creare un nuovo oggetto con le nostre modifiche nella chiamata ricorsiva. In questo modo, ogni fotogramma è un'istantanea senza stato del gioco, in cui ogni fotogramma crea semplicemente un oggetto nuovo di zecca con le modifiche desiderate di qualsiasi oggetto senza stato che necessita di essere aggiornato. 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 imperative e funzionali sono identiche, ma la versione funzionale chiaramente non utilizza uno stato mutabile. Il codice funzionale mantiene tutto lo stato presente nello stack - la cosa bella di questo approccio è che, se qualcosa va storto, il debug è facile, tutto ciò che serve è una traccia dello stack.

Questo aumenta fino a qualsiasi numero di oggetti nel gioco, poiché tutti gli oggetti (o raccolte di oggetti correlati) possono essere renderizzati nel proprio thread.

Quasi ogni applicazione utente che mi viene in mente coinvolge lo stato come un concetto chiave.

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

using System;

namespace ConsoleApplication1
{
    static class Stack
    {
        public static Stack<T> Cons<T>(T hd, Stack<T> tl) { return new Stack<T>(hd, tl); }
        public static Stack<T> Append<T>(Stack<T> x, Stack<T> y)
        {
            return x == null ? y : Cons(x.Head, Append(x.Tail, y));
        }
        public static void Iter<T>(Stack<T> x, Action<T> f) { if (x != null) { f(x.Head); Iter(x.Tail, f); } }
    }

    class Stack<T>
    {
        public readonly T Head;
        public readonly Stack<T> Tail;
        public Stack(T hd, Stack<T> tl)
        {
            this.Head = hd;
            this.Tail = tl;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> x = Stack.Cons(1, Stack.Cons(2, Stack.Cons(3, Stack.Cons(4, null))));
            Stack<int> y = Stack.Cons(5, Stack.Cons(6, Stack.Cons(7, Stack.Cons(8, null))));
            Stack<int> z = Stack.Append(x, y);
            Stack.Iter(z, a => Console.WriteLine(a));
            Console.ReadKey(true);
        }
    }
}

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

type 'a stack =
    | Cons of 'a * 'a stack
    | Nil

let rec append x y =
    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 x y
iter (fun a -> printfn "%i" a) z

Nessun mutevole necessario per creare e manipolare elenchi. 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, cumuli di sinistra, alberi rosso-neri, liste pigre. Non un singolo frammento di codice contiene alcuno stato modificabile. Per "mutare" un albero, ne creo uno nuovo con un nuovo nodo che voglio - questo è molto efficiente perché non ho bisogno di fare una copia di ogni nodo dell'albero, posso riutilizzare quelli vecchi nel mio nuovo albero.

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

La programmazione senza stato è altrettanto espressiva e potente della programmazione con stato, richiede solo un po 'di pratica per allenarsi per iniziare a pensare senza stato. Naturalmente, "programmazione senza stato quando possibile, programmazione con stato ove necessario" sembra essere il motto della maggior parte dei linguaggi funzionali impuri. Non c'è nulla di male nel ricadere sui mutabili quando l'approccio funzionale non è altrettanto pulito o efficiente.


7
Mi piace l'esempio di Pacman. Ma questo potrebbe risolvere un problema solo per sollevarne un altro: cosa succede se qualcos'altro contiene un riferimento all'oggetto Pacman esistente? Quindi non verrà raccolto e sostituito; invece si finiscono con due copie dell'oggetto, una delle quali non è valida. Come gestite questo problema?
Mason Wheeler,

9
Ovviamente devi creare un nuovo "qualcos'altro" con il nuovo oggetto Pacman;) Naturalmente, se prendiamo quella strada troppo lontano, finiamo per ricreare il grafico degli oggetti per il nostro intero mondo ogni volta che qualcosa cambia. Qui viene descritto un approccio migliore ( prog21.dadgum.com/26.html ): piuttosto che avere oggetti che aggiornano se stessi e tutte le loro dipendenze, è molto più facile farli passare messaggi sul loro stato a un ciclo di eventi che gestisce tutti i in aggiornamento. Questo rende molto più semplice decidere quali oggetti nel grafico devono essere aggiornati e quali no.
Giulietta,

6
@Juliet, ho un dubbio: nella mia mentalità totalmente imperativa, la ricorsione deve finire ad un certo punto, altrimenti alla fine produrrai un overflow dello stack. Nell'esempio di pacman ricorsivo, come viene tenuto a bada lo stack: l'oggetto viene implicitamente fatto scoppiare all'inizio della funzione?
BlueStrat,

9
@BlueStrat - buona domanda ... se si tratta di una "chiamata in coda" ... cioè la chiamata ricorsiva è l'ultima cosa nella funzione ... quindi il sistema non ha bisogno di generare un nuovo frame dello stack ... può basta riutilizzare quello precedente. Questa è un'ottimizzazione comune per i linguaggi di programmazione funzionale. en.wikipedia.org/wiki/Tail_call
reteptilian

4
@MichaelOsofsky, quando interagisce con database e API, c'è sempre un "mondo esterno" con cui comunicare. In questo caso, non puoi andare al 100% funzionale. È importante mantenere questo codice "non funzionale" isolato e sottratto in modo che vi sia una sola entrata e un'uscita verso il mondo esterno. In questo modo puoi mantenere funzionale il resto del codice.
Chielt,

76

Risposta breve: non puoi.

Allora, qual è il clamore sull'immutabilità?

Se sei un esperto di linguaggio imperativo, allora sai che "i globuli sono cattivi". 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 possibile. E FP si porta al Santo Graal di modularità: effetti collaterali a tutti . Hai solo la tua f (x) = y. Inserisci x, esci. Nessuna modifica a x o altro. FP ti fa smettere di pensare allo stato e iniziare a pensare in termini di valori. Tutte le tue funzioni semplicemente ricevono 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 interferirà e causerà l'arresto anomalo di una parte funzionante esistente.

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

Terzo, ci sono alcuni possibili vantaggi in termini di prestazioni. Supponi di avere una funzione:

double x = 2 * x

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

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

ma potrei anche fare

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

Il compilatore imperativo non sa se avrò effetti collaterali o meno, il che rende più difficile l'ottimizzazione (cioè il doppio 2 non deve essere 4 ogni volta). Quello funzionale sa che non lo farò, quindi può ottimizzare ogni volta che vede "double 2".

Ora, anche se la creazione di nuovi valori ogni volta sembra incredibilmente dispendiosa per tipi complessi di valori in termini di memoria del computer, non deve essere così. Perché, se hai f (x) = y, e i valori x e y sono "per lo più gli stessi" (ad esempio alberi che differiscono solo in alcune foglie) allora xey possono condividere parti della memoria - perché nessuna di esse 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à, l'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 lo stesso valore di stringa ogni volta. Non troppo utile

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

E, naturalmente, i linguaggi funzionali sono accompagnati da una miriade di altri gadget che rendono la programmazione più efficiente, come funzioni di prima classe ecc.


2
<< readFile ("myFile.txt") dovrebbe restituire lo stesso valore di stringa ogni volta. Non troppo utile. >> Immagino che sia utile finché nascondi il globale, un filesystem. Se lo consideri come un secondo parametro e consenti ad altri processi di restituire un nuovo riferimento al filesystem ogni volta che lo modificano con filesystem2 = write (filesystem1, fd, pos, "string") e lasci che tutti i processi scambino il loro riferimento al filesystem , potremmo ottenere un'immagine molto più pulita del sistema operativo.
Eel GhEEz,

@eelghEEz, questo è lo stesso approccio adottato da Datomic nei database.
Jason,

1
+1 per il confronto chiaro e conciso tra paradigmi. Un suggerimento è int double(x){ return x * (++y); }dato che quello attuale sarà ancora 4, sebbene abbia ancora un effetto collaterale non pubblicizzato, mentre ++yrestituirà 6.
BrainFRZ

@eelghEEz Non sono sicuro di un'alternativa, davvero, è qualcun altro? Per introdurre informazioni in un contesto (puro) di FP, "prendi una misura", ad es. "Al timestamp X, la temperatura è Y". Se qualcuno chiede la temperatura, può implicitamente significare X = ora, ma non può chiedere la temperatura come funzione universale del tempo, giusto? FP si occupa dello stato immutabile e devi creare uno stato immutabile - da fonti interne ed esterne - da uno mutabile. Indici, timestamp, ecc. Sono utili ma ortogonali alla mutabilità - come VCS è il controllo della versione stessa.
Giovanni P

29

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

Questa è una grossolana semplificazione, ma immagina 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 facendo in modo che i metodi prendano oggetti contenenti tutti i valori di cui hanno bisogno per i loro calcoli e quindi restituendo nuovi oggetti con il risultato (forse anche una nuova istanza dello stesso oggetto).

Può essere "difficile" tradurre il codice esistente in questo paradigma, ma è perché richiede davvero un modo completamente diverso di pensare al codice. Come effetto collaterale, sebbene nella maggior parte dei casi si ottengano molte opportunità di parallelismo gratuitamente.

Addendum: (per quanto riguarda la modifica di come tenere traccia dei valori che devono essere modificati)
, sarebbero ovviamente memorizzati in una struttura di dati immutabile ...

Questa non è una 'soluzione' suggerita, ma il modo più semplice per vedere che funzionerà sempre è che potresti memorizzare questi valori immutabili in una struttura simile a una mappa (dizionario / hashtable), digitata da un 'nome variabile'.

Ovviamente nelle soluzioni pratiche useresti un approccio più sano, ma questo dimostra che nel peggiore dei casi, se non funzionasse nient'altro, potresti "simulare" lo stato mutevole con una tale mappa da portare in giro attraverso l'albero di invocazione.


2
OK, ho cambiato il titolo. La tua risposta sembra portare a un problema ancora peggiore, però. Se devo ricreare ogni oggetto ogni volta che qualcosa cambia nel suo stato, passerò tutto il tempo della mia CPU a fare altro che costruire oggetti. Sto pensando alla programmazione del gioco qui, dove hai molte cose che si muovono sullo schermo (e fuori dallo schermo) contemporaneamente, che devono essere in grado di interagire tra loro. L'intero motore ha un framerate prestabilito: tutto ciò che farai, devi farlo in X numero di millisecondi. Sicuramente c'è un modo migliore di riciclare costantemente interi oggetti?
Mason Wheeler,

4
Il bello è che l'imutabilità è sul linguaggio, non sull'implementazione. Con alcuni trucchi, puoi avere uno stato immutabile nella lingua mentre l'implementazione sta infatti cambiando lo stato in atto. Vedi ad esempio la monade ST di Haskell.
CesarB,

4
@Mason: Il punto è che il compilatore può decidere molto meglio dove è (thread) sicuro cambiare lo stato sul posto di quanto tu possa fare.
jerryjvl,

Penso che per i giochi dovresti evitare di immutabile per tutte le parti in cui la velocità non ha importanza. Mentre un linguaggio immutabile potrebbe ottimizzare per te, nulla sarà più veloce della modifica della memoria che le CPU sono veloci nel fare. Quindi, se si scopre che ci sono 10 o 20 posti in cui è necessario un imperativo, penso che dovresti semplicemente evitare l'immutabile del tutto, a meno che tu non possa modulare per aree molto separate come i menu di gioco. E la logica di gioco in particolare potrebbe essere un bel posto da usare immutabile perché mi sento ottimo per fare modellazioni complesse di sistemi puri come le regole di business.
LegendLength

@LegendLength ti stai contraddicendo.
Ixx

18

Penso che ci sia un leggero malinteso. I programmi funzionali puri hanno stato. La differenza è come viene modellato quello stato. Nella pura programmazione funzionale, lo stato è manipolato da funzioni che prendono un certo 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 mutabile globale può essere modellato in questo modo. In Haskell, ad esempio, un programma è una funzione da un mondo a un mondo. Cioè, passi nell'intero universo e il programma restituisce un nuovo universo. In pratica, tuttavia, devi solo passare nelle parti dell'universo in cui il tuo programma è effettivamente interessato. E i programmi in realtà restituiscono una sequenza di azioni che servono come istruzioni per l'ambiente operativo in cui il programma viene eseguito.

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;

Codice imperativo abbastanza standard. Non fa nulla di interessante, ma va bene per l'illustrazione. Penso che sarai d'accordo sul fatto che ci sia stato coinvolto qui. Il valore della variabile x cambia nel tempo. Ora cambiamo leggermente la notazione inventando una nuova sintassi:

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

Metti le parentesi per chiarire cosa significa:

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

Come 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.


È un po 'come una Monade?
CMCDragonkai,

Considereresti questo: A è dichiarativo al livello 1 B è dichiarativo al livello 2, considera A imperativo. C è dichiarativo a livello 3, considera B indispensabile. Quando aumentiamo il livello di astrazione, considera sempre le lingue più basse sul livello di astrazione più imperative di se stesso.
CMCDragonkai,

14

Ecco come si scrive il codice senza stato modificabile : invece di inserire il cambiamento di stato in variabili mutabili, lo si inserisce nei parametri delle funzioni. E invece di scrivere loop, scrivi funzioni ricorsive. Quindi ad 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 (sintassi simile allo schema):

(define (f-functional y) 
  (letrec (
     (f-helper (lambda (x y)
                  (if (p x y) 
                     (f-helper (g x y) y)
                     (h x y)))))
     (f-helper e y)))

o questo codice Haskellish

f_fun y = h x_final y
   where x_initial = e
         x_final   = loop x_initial
         loop x = if p x y then loop (g x y) else x

Per quanto riguarda il motivo per cui ai programmatori funzionali piace fare questo (cosa che non hai chiesto), più parti del tuo programma sono apolidi, più modi ci sono di unire le parti senza che si rompa nulla . Il potere del paradigma apolide non risiede nell'apolidia (o purezza) in , 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 conta .


13

Sono solo modi diversi di fare la stessa cosa.

Prendi in considerazione un semplice esempio come aggiungere i numeri 3, 5 e 10. Immagina di pensare a ciò cambiando prima il valore di 3 aggiungendo 5 ad esso, quindi aggiungendo 10 a quel "3", quindi mostrando il valore corrente di " 3 "(18). Ciò sembra palesemente ridicolo, ma è essenzialmente il modo in cui viene spesso eseguita la programmazione imperativa basata sullo stato. In effetti, puoi avere molti "3" diversi che hanno il valore 3, ma sono diversi. Tutto ciò sembra strano, perché siamo stati così radicati nell'idea, abbastanza enormemente sensata, che i numeri sono immutabili.

Ora pensa ad aggiungere 3, 5 e 10 quando consideri i valori immutabili. Aggiungete 3 e 5 per produrre un altro valore, 8, quindi aggiungete 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 forme diverse. In uno l'informazione esiste come stato e nelle regole per cambiare stato. Nell'altro le informazioni esistono in dati immutabili e definizioni funzionali.


10

Sono in ritardo alla discussione, ma volevo aggiungere alcuni punti per le persone che lottano con la programmazione funzionale.

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

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 di provenienza imperativa siano effettivamente in grado di leggere questo codice. Quindi se non usi molto l'operatore ternario (l'ho sempre evitato nei miei giorni imperativi) ecco come funziona.

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

È possibile concatenare l'espressione ternaria mettendo una nuova espressione ternaria al posto dell'espressione falsa

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 stesse spostando le persone in un mondo di gioco, dovresti introdurre effetti collaterali come disegnare la posizione corrente dell'oggetto sullo schermo e introdurre un po 'di ritardo in ogni chiamata in base alla velocità con cui l'oggetto si muove. Ma non avresti ancora bisogno di uno stato mutabile.

  1. La lezione è che i linguaggi funzionali "mutano" lo stato chiamando la funzione con parametri diversi. Ovviamente questo non muta realmente nessuna variabile, ma è così che ottieni 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 del libro "Impara Java" in cui hanno usato la ricorsione per calcolare fattoriale non lo taglia. Hai bisogno di un toolkit di abilità come rendere i processi iterativi dalla ricorsione (questo è il motivo per cui la ricorsione della coda è essenziale per il linguaggio funzionale), continuazioni, invarianti, ecc. Non faresti programmazione OO senza conoscere i modificatori di accesso, le interfacce ecc. Stessa cosa per la programmazione funzionale.

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


8

In effetti è abbastanza facile avere qualcosa che assomigli allo stato mutabile anche in lingue senza stato mutevole.

Considera una funzione con tipo s -> (a, s). Traducendo dalla sintassi 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 prende 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 " a" genere). Se si concatenano più chiamate di funzioni con tipi come questo (ottenere lo stato restituito da una funzione e passarlo alla successiva), si ha uno stato "mutabile" (in effetti ci si trova in ciascuna funzione creando un nuovo stato e abbandonando quello vecchio ).

Potrebbe essere più facile da capire se immagini lo stato mutevole come lo "spazio" in cui il tuo programma è in esecuzione, e poi pensi alla dimensione temporale. All'istante t1, lo "spazio" è in una determinata condizione (ad esempio, una posizione di memoria ha un valore 5). In un secondo momento t2, si trova in una condizione diversa (ad esempio, la posizione di memoria ora ha valore 10). Ognuna di queste "fettine" di tempo è uno stato ed è immutabile (non puoi tornare indietro nel tempo per cambiarle). Quindi, da questo punto di vista, sei passato dall'intero spaziotempo con una freccia temporale (il tuo stato mutabile) a una serie di porzioni di spaziotempo (diversi stati immutabili), e il tuo programma sta semplicemente trattando ogni fetta come un valore e calcolando ciascuna di essi come funzione applicata alla precedente.

OK, forse non è stato 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 nell'istante successivo (subito dopo averne creato 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 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 contrassegnare la sequenza di esecuzione.

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

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

Traducendo da queste dichiarazioni simili a Haskell, readRefriceve qualcosa che assomiglia a un puntatore o una maniglia a un valore di tipo " a" e allo stato falso e restituisce il valore di tipo " a" a cui punta il primo parametro e un nuovo stato falso. writeRefè simile, ma cambia invece il valore indicato.

Se lo chiami readRefe poi lo passi allo 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 di writeRefnuovo con lo stesso puntatore / handle e scrivere nella stessa posizione di memoria - ma, dal momento che concettualmente restituisce un nuovo stato (falso), lo stato (falso) è ancora imputabile (ne è stato creato uno nuovo "). Il compilatore chiamerà le funzioni nell'ordine in cui dovrebbe chiamarle se ci fosse una variabile di stato reale che doveva essere calcolata, ma l'unico stato in cui si trova è lo stato completo (mutabile) dell'hardware reale.

(Chi conosce Haskell noterà ho semplificato le cose molto e ommited diversi dettagli importanti. Per coloro che vogliono vedere maggiori dettagli, date un'occhiata al Control.Monad.Statedal mtl, e le ST se IOalias ( ST RealWorldmonadi).)

Potresti chiederti perché farlo in maniera così rotonda (invece di avere semplicemente uno stato mutevole nella lingua). Il vero vantaggio è che hai reificato lo stato del tuo programma. Ciò che prima era implicito (lo stato del programma era globale, consentendo 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 fili di stato separati e, con un po 'di magia di tipo, possono essere usati per incorporare un calcolo imperativo in uno puro, senza renderlo impuro (la STmonade in Haskell è quella normalmente usata per questo trucco; il State#sopra menzionato è in effetti GHC State# s, utilizzato dalla sua implementazione di STeIO monadi).


7

La programmazione funzionale evita lo stato e sottolineafunzionalità. Non esiste mai nulla come uno stato, anche se lo stato potrebbe effettivamente essere qualcosa di immutabile o inserito nell'architettura di ciò con cui stai lavorando. Considera la differenza tra un server web statico che carica 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 di file in una risposta dal contenuto di quel file. Praticamente non è necessario alcuno stato oltre una piccola parte della configurazione (lo 'stato' del filesystem è 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.


Quando ero più anti-funzionale, mi chiedevo come potesse essere buono quando qualcosa come un disco rigido è mutevole. Tutte le mie classi c # avevano uno stato mutabile e potevano simulare logicamente un disco rigido o qualsiasi altro dispositivo. Considerando che con funzionale c'era una discrepanza tra i modelli e le macchine reali che stavano modellando. Dopo aver approfondito le funzionalità, ho capito che i vantaggi che ottieni sono in grado di superare un bel po 'questo problema. E se fosse fisicamente possibile inventare un disco rigido che ne ha fatto una copia, sarebbe effettivamente utile (come già fa il journaling).
LegendLength

5

Oltre alle grandi risposte che altri stanno dando, pensa alle lezioni Integere Stringin Java. Le istanze di queste classi sono immutabili, ma ciò non le rende inutili solo perché le loro istanze non possono essere modificate. L'immutabilità ti dà un po 'di sicurezza. Sai se usi un'istanza String o Integer come chiave per a Map, la chiave non può essere cambiata. Confronta questo con la Dateclasse in Java:

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

Hai silenziosamente cambiato 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ù facile per il programmatore e anche più semplice per l'ottimizzatore.


2
Lo capisco, ma non risponde alla mia domanda. Tenendo presente che un programma per computer è un modello di qualche evento o processo del mondo reale, se non è possibile modificare i propri valori, come modellare qualcosa che cambia?
Mason Wheeler,

Bene, puoi certamente fare cose utili con le classi Integer e String. Non è come se la loro immutabilità significasse che non puoi avere uno stato mutabile.
Eddie,

@Mason Wheeler - Comprendendo che una cosa e il suo stato sono due "cose" diverse. Ciò che è pacman non cambia da tempo A a tempo B. Dove cambia pacman cambia. Quando passi dal tempo A al tempo B, ottieni una nuova combinazione di pacman + stato ... che è lo stesso pacman, stato diverso. Stato non modificato ... stato diverso.
RHSeeger,

4

Per applicazioni altamente interattive come i giochi, la Programmazione reattiva funzionale è il tuo amico: se riesci a formulare le proprietà del mondo del tuo gioco come valori variabili nel tempo (e / o flussi di eventi), sei pronto! Queste formule saranno talvolta ancora più naturali e rivelatrici di intenti rispetto alla mutazione di uno stato, ad esempio per una palla in movimento, puoi usare direttamente la nota legge x = v * t . E cosa c'è di meglio, le regole del gioco scritte in questo modo compongono meglio delle astrazioni orientate agli oggetti. Ad esempio, in questo caso, la velocità della palla può anche essere un valore variabile nel tempo, che dipende dal flusso di eventi costituito dalle collisioni della palla. Per considerazioni di progettazione più concrete, vedere Creazione di giochi in Elm .



3

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

La programmazione orientata agli oggetti ci ha unito stato e comportamento, ma è stata una nuova idea quando l'ho incontrata per la prima volta dal C ++ nel 1994.

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


2
Non sarei d'accordo che questo è qualcosa che puoi appuntare su OO. Le lingue prima di OO incoraggiavano lo stato e gli algoritmi di accoppiamento. OO ha appena fornito un modo migliore per gestirlo.
Jason Baker,

"Incoraggiato" - forse. OO lo rende una parte esplicita della lingua. Puoi fare incapsulamento e nascondere informazioni in C, ma direi che le lingue OO lo rendono molto più semplice.
duffymo,

2

Ricorda: i linguaggi funzionali sono Turing completi. Pertanto, qualsiasi compito utile che eseguiresti in un linguaggio imperativo può essere svolto in un linguaggio funzionale. Alla fine, però, penso che ci sia qualcosa da dire su un approccio ibrido. Linguaggi come F # e Clojure (e ne sono certo altri) incoraggiano la progettazione senza stato, ma consentono la mutabilità quando necessario.


Solo perché due lingue sono complete, Turing non significa che possono svolgere le stesse attività. Ciò significa che possono eseguire lo stesso calcolo. Brainfuck è Turing completo, ma sono abbastanza certo che non possa comunicare su uno stack TCP.
RHSeeger,

2
Certo che può. Dato lo stesso accesso all'hardware di C, può farlo. Ciò non significa che sarebbe pratico, ma la possibilità è lì.
Jason Baker,

2

Non puoi avere un linguaggio funzionale puro che sia utile. Ci sarà sempre un livello di mutabilità che devi affrontare, IO è un esempio.

Pensa ai linguaggi funzionali come a un altro strumento che usi. È buono per certe cose, ma non per altre. L'esempio di gioco che hai fornito potrebbe non essere il modo migliore per usare un linguaggio funzionale, almeno lo schermo avrà uno stato mutevole di cui non puoi fare nulla con FP. Il modo in cui pensi al problema e il tipo di problemi che risolvi con FP sarà diverso da quelli a cui sei abituato con la programmazione imperativa.



-3

Questo è molto semplice Puoi usare tutte le variabili che vuoi nella programmazione funzionale ... ma solo se sono variabili locali (contenute all'interno di funzioni). Quindi racchiudi il tuo codice in funzioni, passa i valori avanti e indietro tra quelle funzioni (come parametri passati e valori restituiti) ... e questo è tutto!

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()
    )
);

John, che lingua è questa?
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.