Vantaggi della programmazione senza stato?


132

Recentemente ho imparato a conoscere la programmazione funzionale (in particolare Haskell, ma ho anche seguito tutorial su Lisp ed Erlang). Mentre ho trovato i concetti molto illuminanti, non vedo ancora il lato pratico del concetto "senza effetti collaterali". Quali sono i vantaggi pratici di esso? Sto cercando di pensare nella mentalità funzionale, ma ci sono alcune situazioni che sembrano troppo complesse senza la possibilità di salvare lo stato in modo semplice (non considero le monadi di Haskell "facili").

Vale la pena continuare a studiare in profondità l'Haskell (o un altro linguaggio puramente funzionale)? La programmazione funzionale o apolide è effettivamente più produttiva che procedurale? È probabile che in seguito continuerò a utilizzare Haskell o un altro linguaggio funzionale o dovrei impararlo solo per la comprensione?

Mi preoccupo meno delle prestazioni che della produttività. Quindi chiedo principalmente se sarò più produttivo in un linguaggio funzionale rispetto a un procedimento / orientato agli oggetti / qualunque cosa.

Risposte:


168

Leggi la programmazione funzionale in breve .

Ci sono molti vantaggi nella programmazione senza stato, non ultimo dei quali è un codice drammaticamente multithread e simultaneo. Per dirla senza mezzi termini, lo stato mutevole è nemico del codice multithread. Se i valori sono immutabili per impostazione predefinita, i programmatori non devono preoccuparsi di un thread che muta il valore dello stato condiviso tra due thread, quindi elimina un'intera classe di bug multithreading relativi alle condizioni della competizione. Dato che non ci sono condizioni di gara, non c'è motivo di usare nemmeno i blocchi, quindi l'immutabilità elimina anche un'altra intera classe di bug relativi ai deadlock.

Questo è il motivo principale per cui conta la programmazione funzionale, e probabilmente il migliore per saltare sul treno di programmazione funzionale. Ci sono anche molti altri vantaggi, tra cui il debug semplificato (ovvero le funzioni sono pure e non mutano lo stato in altre parti di un'applicazione), un codice più conciso ed espressivo, un codice meno comprensivo rispetto alle lingue che sono fortemente dipendenti dai modelli di progettazione e il compilatore può ottimizzare più aggressivamente il tuo codice.


5
Io secondo questo! Credo che la programmazione funzionale sarà utilizzata molto più ampiamente in futuro a causa della sua idoneità alla programmazione parallela.
Ray Hidayat,

@Ray: aggiungerei anche la programmazione distribuita!
Anton Tykhyy,

Gran parte di ciò è vero, ad eccezione del debug. Di solito è più difficile in haskell, perché non hai un vero stack di chiamate, solo uno stack di pattern-match. Ed è molto più difficile prevedere come finirà il tuo codice.
Hasufell,

3
Inoltre, la programmazione funzionale non riguarda in realtà "senza stato". La ricorsione è già uno stato (locale) implicito ed è la cosa principale che facciamo in haskell. Ciò diventa chiaro una volta implementati alcuni algoritmi non banali in haskell idiomatico (ad esempio roba di geometria computazionale) e divertiti a eseguire il debug di questi.
Hasufell,

2
Non mi piace equiparare apolidi a FP. Molti programmi FP sono pieni di stato, esiste semplicemente in una chiusura piuttosto che in un oggetto.
mikemaccana,

46

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 Why Functional Programming Matters (PDF).

Sarete gobs più produttivo, soprattutto se si sceglie un linguaggio funzionale che ha anche tipi di dati algebrici e pattern matching (Caml, SML, Haskell).


I mixin non fornirebbero anche codice riutilizzabile in modo simile con OOP? Non sostenere OOP, ma solo cercare di capire le cose da solo.
mikemaccana,

20

Molte delle altre risposte si sono concentrate sul lato prestazionale (parallelismo) della programmazione funzionale, che credo sia molto importante. Tuttavia, hai chiesto in modo specifico la produttività, come in, puoi programmare la stessa cosa più velocemente in un paradigma funzionale che in un paradigma imperativo.

In realtà trovo (per esperienza personale) che la programmazione in F # corrisponda al modo in cui penso meglio, quindi è più facile. Penso che sia la differenza più grande. Ho programmato sia in F # che in C #, e c'è molto meno "combattere la lingua" in F #, che adoro. Non devi pensare ai dettagli in F #. Ecco alcuni esempi di ciò che ho scoperto che mi piace molto.

Ad esempio, anche se F # è tipizzato staticamente (tutti i tipi vengono risolti in fase di compilazione), l'inferenza del tipo determina quali tipi hai, quindi non devi dirlo. E se non riesce a capirlo, rende automaticamente la tua funzione / classe / qualunque cosa generica. Quindi non devi mai scrivere alcun generico, è tutto automatico. Trovo che ciò significhi che sto spendendo più tempo a pensare al problema e meno a come implementarlo. In effetti, ogni volta che torno a C #, trovo che mi manca davvero questa inferenza di tipo, non ti rendi mai conto di quanto sia fastidioso fino a quando non devi più farlo.

Anche in F #, invece di scrivere loop, chiamate funzioni. È un cambiamento sottile, ma significativo, perché non devi più pensare al costrutto del ciclo. Ad esempio, ecco un pezzo di codice che passa attraverso e abbina qualcosa (non ricordo cosa, proviene da un puzzle di Eulero del progetto):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Mi rendo conto che fare un filtro quindi una mappa (che è una conversione di ogni elemento) in C # sarebbe abbastanza semplice, ma devi pensare a un livello inferiore. In particolare, dovresti scrivere il ciclo stesso e avere la tua istruzione if esplicita e quel tipo di cose. Da quando ho imparato F #, mi sono reso conto che ho trovato più facile programmare in modo funzionale, dove se vuoi filtrare, scrivi "filter", e se vuoi mappare, scrivi "map", invece di implementare ciascuno dei dettagli.

Adoro anche l'operatore |>, che secondo me separa F # da ocaml, e probabilmente altri linguaggi funzionali. È l'operatore pipe, ti permette di "pipe" l'output di un'espressione nell'input di un'altra espressione. Fa seguire il codice come penso di più. Come nello snippet di codice sopra, si dice "prendi la sequenza dei fattori, filtrala, quindi mappala". È un livello molto alto di pensiero, che non si ottiene in un linguaggio di programmazione imperativo perché sei così impegnato a scrivere il ciclo e le dichiarazioni if. È l'unica cosa che mi manca di più ogni volta che vado in un'altra lingua.

Quindi, in generale, anche se posso programmare sia in C # che in F #, trovo più facile usare F # perché si può pensare a un livello superiore. Direi che, poiché i dettagli più piccoli vengono rimossi dalla programmazione funzionale (almeno in F #), sono più produttivo.

Modifica : ho visto in uno dei commenti che hai chiesto un esempio di "stato" in un linguaggio di programmazione funzionale. F # può essere scritto in modo imperativo, quindi ecco un esempio diretto di come puoi avere uno stato mutabile in F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i

1
Sono d'accordo con il tuo post in generale, ma |> non ha nulla a che fare con la programmazione funzionale in sé. In realtà, a |> b (p1, p2)è solo zucchero sintattico per b (a, p1, p2). Associa questo alla giusta associatività e il gioco è fatto.
Anton Tykhyy,

2
È vero, dovrei riconoscere che probabilmente molta della mia esperienza positiva con F # ha più a che fare con F # che con la programmazione funzionale. Tuttavia, esiste una forte correlazione tra i due, e anche se cose come l'inferenza del tipo e |> non sono di per sé una programmazione funzionale, certamente direi che "vanno con il territorio". Almeno in generale.
Ray Hidayat,

|> è solo un'altra funzione di infissione di ordine superiore, in questo caso un operatore applicazione-funzione. Definire il tuo ordine superiore, gli operatori infix fanno sicuramente parte della programmazione funzionale (a meno che tu non sia uno Schemer). Haskell ha i suoi $, che è lo stesso, tranne le informazioni nel flusso della pipeline da destra a sinistra.
Norman Ramsey,

15

Considera tutti i bug difficili che hai impiegato a lungo nel debug.

Ora, quanti di questi bug erano dovuti a "interazioni indesiderate" tra due componenti separate di un programma? (Quasi tutti i bug di threading hanno questa forma: gare che coinvolgono la scrittura di dati condivisi, deadlock, ... Inoltre, è comune trovare librerie che abbiano un effetto inaspettato sullo stato globale, o leggere / scrivere il registro / ambiente, ecc.) I Supporterebbe che almeno 1 su 3 "bug" rientri in questa categoria.

Ora se passi alla programmazione senza stato / immutabile / pura, tutti questi bug scompaiono. Ti vengono presentate alcune nuove sfide (ad esempio quando lo fai desiderare diversi moduli di interagire con l'ambiente), ma in un linguaggio come Haskell, queste interazioni vengono esplicitamente reificati nel sistema tipo, il che significa che si può solo guardare il tipo di una funzione e una ragione sul tipo di interazioni che può avere con il resto del programma.

Questa è la grande vittoria dell'IMO "immutabilità". In un mondo ideale, progetteremmo tutte fantastiche API e anche quando le cose fossero mutabili, gli effetti sarebbero locali e ben documentati e le interazioni "inattese" sarebbero ridotte al minimo. Nel mondo reale, ci sono molte API che interagiscono con lo stato globale in una miriade di modi, e questi sono la fonte dei bug più dannosi. Aspirare all'apolidia aspira a liberarsi delle interazioni non intenzionali / implicite / dietro le quinte tra i componenti.


6
Qualcuno una volta ha detto che sovrascrivere un valore mutabile significa che stai esplicitamente spazzatura raccogliendo / liberando il valore precedente. In alcuni casi, altre parti del programma non sono state eseguite utilizzando quel valore. Quando i valori non possono essere mutati, anche questa classe di bug scompare.
shapr,

8

Un vantaggio delle funzioni senza stato è che consentono il calcolo preliminare o la memorizzazione nella cache dei valori restituiti della funzione. Anche alcuni compilatori C consentono di contrassegnare esplicitamente le funzioni come apolidi per migliorarne l'ottimizzazione. Come molti altri hanno notato, le funzioni senza stato sono molto più facili da parallelizzare.

Ma l'efficienza non è l'unica preoccupazione. Una funzione pura è più facile da testare ed eseguire il debug poiché tutto ciò che la influenza viene esplicitamente dichiarato. E quando si programma in un linguaggio funzionale, si ha l'abitudine di rendere il minor numero possibile di funzioni "sporche" (con I / O, ecc.). Separare le cose con stato in questo modo è un buon modo per progettare programmi, anche in linguaggi non così funzionali.

I linguaggi funzionali possono richiedere un po 'di tempo per "ottenere", ed è difficile da spiegare a qualcuno che non ha attraversato quel processo. Ma la maggior parte delle persone che persistono abbastanza a lungo finalmente si rendono conto che ne vale la pena, anche se non usano molto i linguaggi funzionali.


Quella prima parte è un punto davvero interessante, non ci avevo mai pensato prima. Grazie!
Sasha Chedygov,

Supponiamo di avere sin(PI/3)nel tuo codice, dove PI è una costante, il compilatore potrebbe valutare questa funzione al momento della compilazione e incorporare il risultato nel codice generato.
Artelius

6

Senza stato, è molto semplice parallelizzare automaticamente il codice (poiché le CPU sono realizzate con un numero sempre maggiore di core, questo è molto importante).


Sì, l'ho sicuramente esaminato. Il modello di concorrenza di Erlang in particolare è molto intrigante. Tuttavia, a questo punto non mi interessa davvero tanto la concorrenza quanto la produttività. Esiste un bonus di produttività dalla programmazione senza stato?
Sasha Chedygov,

2
@musicfreak, no non c'è un bonus di produttività. Ma come nota, i moderni linguaggi FP ti consentono di usare lo stato se ne hai davvero bisogno.
Sconosciuto

Veramente? Puoi dare un esempio di stato in un linguaggio funzionale, solo così posso vedere come è fatto?
Sasha Chedygov,

Controlla lo Stato Monade in Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
rampion

4
@Sconosciuta: non sono d'accordo. La programmazione senza stato riduce la presenza di bug dovuti a interazioni impreviste / indesiderate di diversi componenti. Incoraggia anche una migliore progettazione (maggiore riusabilità, separazione di meccanismi e politiche e quel genere di cose). Non è sempre appropriato per l'attività da svolgere, ma in alcuni casi brilla davvero.
Artelius

6

Le applicazioni Web senza stato sono essenziali quando si avvia un traffico più elevato.

Ad esempio, potrebbero esserci molti dati utente che non si desidera archiviare sul lato client per motivi di sicurezza. In questo caso è necessario memorizzarlo sul lato server. È possibile utilizzare la sessione predefinita delle applicazioni Web ma se si dispone di più di un'istanza dell'applicazione, è necessario assicurarsi che ciascun utente sia sempre indirizzato alla stessa istanza.

I sistemi di bilanciamento del carico spesso hanno la possibilità di avere "sessioni permanenti" in cui il sistema di bilanciamento del carico sa come a quale server inviare gli utenti. Questo non è l'ideale però, ad esempio significa che ogni volta che riavvii la tua applicazione web, tutti gli utenti connessi perderanno la sessione.

Un approccio migliore è quello di archiviare la sessione dietro i server Web in una sorta di archivio dati, in questi giorni ci sono un sacco di fantastici prodotti nosql disponibili per questo (redis, mongo, elasticsearch, memcached). In questo modo i server Web sono senza stato ma si ha ancora il lato server di stato e la disponibilità di questo stato può essere gestita scegliendo la corretta configurazione del datastore. Questi archivi dati di solito hanno una grande ridondanza, quindi dovrebbe quasi sempre essere possibile apportare modifiche all'applicazione Web e persino all'archivio dati senza influire sugli utenti.


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.