Padroneggia l'intervista a JavaScript: che cos'è la programmazione funzionale?

Struttura Synth - Orihaus (CC BY 2.0)
"Padroneggia l'intervista a JavaScript" è una serie di post progettati per preparare i candidati alle domande più comuni che possono incontrare quando fanno domanda per una posizione JavaScript di livello medio-alto. Queste sono domande che utilizzo frequentemente in interviste reali.

La programmazione funzionale è diventata un argomento molto caldo nel mondo JavaScript. Solo pochi anni fa, pochi programmatori JavaScript sapevano persino cosa fosse la programmazione funzionale, ma ogni base di codice di applicazioni di grandi dimensioni che ho visto negli ultimi 3 anni fa un forte uso delle idee di programmazione funzionale.

La programmazione funzionale (spesso FP abbreviata) è il processo di creazione di software componendo funzioni pure, evitando lo stato condiviso, i dati mutabili e gli effetti collaterali. La programmazione funzionale è dichiarativa piuttosto che imperativa e lo stato dell'applicazione scorre attraverso funzioni pure. Contrasto con la programmazione orientata agli oggetti, in cui lo stato dell'applicazione è generalmente condiviso e associato ai metodi negli oggetti.

La programmazione funzionale è un paradigma di programmazione, nel senso che è un modo di pensare alla costruzione del software basato su alcuni principi fondamentali e definitivi (elencati sopra). Altri esempi di paradigmi di programmazione includono la programmazione orientata agli oggetti e la programmazione procedurale.

Il codice funzionale tende ad essere più conciso, più prevedibile e più facile da testare rispetto al codice imperativo o orientato agli oggetti, ma se non si ha familiarità con esso e con i modelli comuni ad esso associati, il codice funzionale può anche sembrare molto più denso e il la letteratura correlata può essere impenetrabile per i nuovi arrivati.

Se inizi a cercare su Google termini di programmazione funzionale, raggiungerai rapidamente un muro di mattoni di gergo accademico che può essere molto intimidatorio per i principianti. Dire che ha una curva di apprendimento è un eufemismo serio. Ma se hai programmato JavaScript per un po ', è probabile che tu abbia usato molti concetti e utilità di programmazione funzionale nel tuo vero software.

Non lasciare che tutte le nuove parole ti spaventino. È molto più semplice di quanto sembri.

La parte più difficile è avvolgere la testa attorno a tutto il vocabolario sconosciuto. Ci sono molte idee nella definizione dall'aspetto innocente sopra le quali tutte devono essere comprese prima di poter iniziare a comprendere il significato della programmazione funzionale:

  • Funzioni pure
  • Composizione delle funzioni
  • Evita lo stato condiviso
  • Evitare lo stato mutante
  • Evita gli effetti collaterali

In altre parole, se vuoi sapere cosa significa in pratica la programmazione funzionale, devi iniziare con la comprensione di quei concetti chiave.

Una funzione pura è una funzione che:

  • Dati gli stessi input, restituisce sempre lo stesso output e
  • Non ha effetti collaterali

Le funzioni pure hanno molte proprietà importanti nella programmazione funzionale, inclusa la trasparenza referenziale (è possibile sostituire una chiamata di funzione con il valore risultante senza cambiare il significato del programma). Leggi "Cos'è una funzione pura?" Per maggiori dettagli.

La composizione delle funzioni è il processo di combinazione di due o più funzioni per produrre una nuova funzione o eseguire alcuni calcoli. Ad esempio, la composizione f. g (il punto significa "composto con") equivale a f (g (x)) in JavaScript. Comprendere la composizione delle funzioni è un passo importante verso la comprensione di come viene costruito il software utilizzando la programmazione funzionale. Leggi "Cos'è la composizione delle funzioni?" Per ulteriori informazioni.

Stato condiviso

Lo stato condiviso è qualsiasi variabile, oggetto o spazio di memoria esistente in un ambito condiviso o come proprietà di un oggetto che viene passato tra ambiti. Un ambito condiviso può includere ambiti globali o ambiti di chiusura. Spesso, nella programmazione orientata agli oggetti, gli oggetti vengono condivisi tra gli ambiti aggiungendo proprietà ad altri oggetti.

Ad esempio, un gioco per computer potrebbe avere un oggetto di gioco principale, con personaggi e oggetti di gioco memorizzati come proprietà di proprietà di quell'oggetto. La programmazione funzionale evita lo stato condiviso, basandosi invece su strutture di dati immutabili e calcoli puri per ricavare nuovi dati da dati esistenti. Per maggiori dettagli su come il software funzionale potrebbe gestire lo stato dell'applicazione, vedere "10 suggerimenti per una migliore architettura Redux".

Il problema con lo stato condiviso è che per comprendere gli effetti di una funzione, è necessario conoscere l'intera cronologia di ogni variabile condivisa che la funzione utilizza o influisce.

Immagina di avere un oggetto utente che deve essere salvato. La funzione saveUser () effettua una richiesta a un'API sul server. Mentre ciò accade, l'utente cambia la propria immagine del profilo con updateAvatar () e attiva un'altra richiesta saveUser (). Al momento del salvataggio, il server restituisce un oggetto utente canonico che dovrebbe sostituire tutto ciò che è in memoria per sincronizzarsi con le modifiche che si verificano sul server o in risposta ad altre chiamate API.

Sfortunatamente, la seconda risposta viene ricevuta prima della prima risposta, quindi quando viene restituita la prima risposta (ora obsoleta), la nuova immagine del profilo viene cancellata in memoria e sostituita con quella precedente. Questo è un esempio di una razza - un bug molto comune associato allo stato condiviso.

Un altro problema comune associato allo stato condiviso è che la modifica dell'ordine in cui vengono chiamate le funzioni può causare una cascata di guasti perché le funzioni che agiscono sullo stato condiviso dipendono dal tempo:

Quando si evita lo stato condiviso, i tempi e l'ordine delle chiamate di funzione non cambiano il risultato della chiamata alla funzione. Con le funzioni pure, dato lo stesso input, otterrai sempre lo stesso output. Ciò rende le chiamate di funzione completamente indipendenti dalle altre chiamate di funzione, il che può semplificare radicalmente le modifiche e il refactoring. Un cambiamento in una funzione o la tempistica di una chiamata di funzione non si interromperanno e spezzeranno altre parti del programma.

Nell'esempio sopra, usiamo Object.assign () e passiamo un oggetto vuoto come primo parametro per copiare le proprietà di x invece di mutarlo in posizione. In questo caso, sarebbe stato equivalente semplicemente a creare un nuovo oggetto da zero, senza Object.assign (), ma questo è un modello comune in JavaScript per creare copie dello stato esistente invece di utilizzare le mutazioni, che abbiamo dimostrato nel primo esempio.

Se osservi attentamente le istruzioni console.log () in questo esempio, dovresti notare qualcosa che ho già menzionato: composizione della funzione. Ricordiamo da prima, la composizione della funzione è simile alla seguente: f (g (x)). In questo caso, sostituiamo f () e g () con x1 () e x2 () per la composizione: x1. x2.

Naturalmente, se si modifica l'ordine della composizione, l'output cambierà. L'ordine delle operazioni è ancora importante. f (g (x)) non è sempre uguale a g (f (x)), ma ciò che non importa più è cosa succede alle variabili al di fuori della funzione - e questo è un grosso problema. Con le funzioni impure, è impossibile comprendere appieno cosa fa una funzione se non si conosce l'intera cronologia di ogni variabile che la funzione utilizza o influenza.

Rimuovere la dipendenza del timing delle chiamate di funzione ed eliminare un'intera classe di potenziali bug.

Immutabilità

Un oggetto immutabile è un oggetto che non può essere modificato dopo la sua creazione. Al contrario, un oggetto mutabile è qualsiasi oggetto che può essere modificato dopo la sua creazione.

L'immutabilità è un concetto centrale di programmazione funzionale perché senza di essa, il flusso di dati nel programma è in perdita. La storia dello stato viene abbandonata e strani bug possono insinuarsi nel tuo software. Per ulteriori informazioni sul significato dell'immutabilità, vedere "Il Dao dell'immutabilità".

In JavaScript, è importante non confondere const, con l'immutabilità. const crea un'associazione con nome variabile che non può essere riassegnata dopo la creazione. const non crea oggetti immutabili. Non è possibile modificare l'oggetto a cui si riferisce l'associazione, ma è comunque possibile modificare le proprietà dell'oggetto, il che significa che i collegamenti creati con const sono mutabili, non immutabili.

Gli oggetti immutabili non possono essere cambiati affatto. Puoi rendere un valore veramente immutabile congelando profondamente l'oggetto. JavaScript ha un metodo che congela un oggetto a un livello profondo:

Ma gli oggetti congelati sono solo superficialmente immutabili. Ad esempio, il seguente oggetto è modificabile:

Come puoi vedere, le proprietà primitive di livello superiore di un oggetto congelato non possono cambiare, ma qualsiasi proprietà che è anche un oggetto (compresi array, ecc ...) può ancora essere mutata - quindi anche gli oggetti congelati non sono immutabili a meno che tu non cammini il intero albero degli oggetti e congelare ogni proprietà dell'oggetto.

In molti linguaggi di programmazione funzionale, ci sono speciali strutture di dati immutabili chiamate trie strutture (pronunciate "albero") che sono effettivamente congelate, il che significa che nessuna proprietà può cambiare, indipendentemente dal livello della proprietà nella gerarchia degli oggetti.

I tentativi utilizzano la condivisione strutturale per condividere posizioni di memoria di riferimento per tutte le parti dell'oggetto che rimangono invariate dopo che un oggetto è stato copiato da un operatore, che utilizza meno memoria e consente significativi miglioramenti delle prestazioni per alcuni tipi di operazioni.

Ad esempio, è possibile utilizzare i confronti di identità nella radice di una struttura ad albero per i confronti. Se l'identità è la stessa, non è necessario percorrere l'intero albero per verificare le differenze.

Esistono diverse librerie in JavaScript che sfruttano i tentativi, tra cui Immutable.js e Mori.

Ho sperimentato entrambi e tendo a utilizzare Immutable.js in grandi progetti che richiedono quantità significative di stato immutabile. Per ulteriori informazioni, vedere "10 suggerimenti per una migliore architettura Redux".

Effetti collaterali

Un effetto collaterale è qualsiasi cambiamento dello stato dell'applicazione osservabile al di fuori della funzione chiamata diverso dal suo valore restituito. Gli effetti collaterali includono:

  • Modifica di qualsiasi variabile esterna o proprietà dell'oggetto (ad es. Una variabile globale o una variabile nella catena dell'ambito della funzione padre)
  • Accedere alla console
  • Scrivere sullo schermo
  • Scrivere su un file
  • Scrivere in rete
  • Attivazione di qualsiasi processo esterno
  • Chiamare qualsiasi altra funzione con effetti collaterali

Gli effetti collaterali sono per lo più evitati nella programmazione funzionale, il che rende gli effetti di un programma molto più facili da capire e molto più facili da testare.

Haskell e altri linguaggi funzionali spesso isolano e incapsulano gli effetti collaterali dalle funzioni pure usando le monadi. L'argomento delle monadi è abbastanza approfondito su cui scrivere un libro, quindi lo salveremo per dopo.

Quello che devi sapere in questo momento è che le azioni con effetti collaterali devono essere isolate dal resto del tuo software. Se mantieni gli effetti collaterali separati dal resto della logica del tuo programma, il tuo software sarà molto più facile da estendere, refactor, debug, test e manutenzione.

Questo è il motivo per cui la maggior parte dei framework front-end incoraggia gli utenti a gestire il rendering dello stato e dei componenti in moduli separati, liberamente accoppiati.

Riutilizzabilità attraverso funzioni di ordine superiore

La programmazione funzionale tende a riutilizzare un insieme comune di utilità funzionali per elaborare i dati. La programmazione orientata agli oggetti tende a raggruppare metodi e dati negli oggetti. Questi metodi associati possono operare solo sul tipo di dati su cui sono stati progettati per operare, e spesso solo sui dati contenuti in quella specifica istanza di oggetto.

Nella programmazione funzionale, qualsiasi tipo di dati è un gioco equo. La stessa utility map () può eseguire il mapping su oggetti, stringhe, numeri o qualsiasi altro tipo di dati perché utilizza una funzione come argomento che gestisce in modo appropriato il tipo di dati specificato. FP risolve il suo inganno di utilità generica utilizzando funzioni di ordine superiore.

JavaScript ha funzioni di prima classe, che ci consentono di trattare le funzioni come dati: assegnarle a variabili, passarle ad altre funzioni, restituirle da funzioni, ecc ...

Una funzione di ordine superiore è qualsiasi funzione che accetta una funzione come argomento, restituisce una funzione o entrambi. Le funzioni di ordine superiore vengono spesso utilizzate per:

  • Astrazione o isolamento di azioni, effetti o controllo asincrono del flusso mediante funzioni di callback, promesse, monadi, ecc ...
  • Crea utility in grado di agire su un'ampia varietà di tipi di dati
  • Applicare parzialmente una funzione ai suoi argomenti o creare una funzione curry ai fini del riutilizzo o della composizione della funzione
  • Prendi un elenco di funzioni e restituisce una composizione di tali funzioni di input

Contenitori, portatori, elenchi e flussi

Un funzione è qualcosa che può essere mappato. In altre parole, è un contenitore che ha un'interfaccia che può essere utilizzata per applicare una funzione ai valori al suo interno. Quando vedi la parola funzione, dovresti pensare "mappabile".

In precedenza abbiamo appreso che la stessa utility map () può agire su una varietà di tipi di dati. Lo fa sollevando l'operazione di mappatura per lavorare con un'API functor. Le importanti operazioni di controllo del flusso utilizzate da map () sfruttano tale interfaccia. Nel caso di Array.prototype.map (), il contenitore è un array, ma anche altre strutture di dati possono essere funzioni, purché forniscano l'API di mapping.

Diamo un'occhiata a come Array.prototype.map () ti consente di astrarre il tipo di dati dall'utilità di mappatura per rendere map () utilizzabile con qualsiasi tipo di dati. Creeremo una semplice mappatura double () che moltiplica semplicemente i valori passati per 2:

E se vogliamo operare su obiettivi in ​​un gioco per raddoppiare il numero di punti assegnati? Tutto quello che dobbiamo fare è apportare una sottile modifica alla funzione double () che passiamo in map (), e tutto funziona ancora:

Il concetto di utilizzare astrazioni come i funzioni e le funzioni di ordine superiore per utilizzare funzioni di utilità generiche per manipolare un numero qualsiasi di tipi di dati diversi è importante nella programmazione funzionale. Vedrai un concetto simile applicato in tutti i modi.

"Un elenco espresso nel tempo è un flusso."

Tutto ciò che devi capire per ora è che array e funzioni non sono l'unico modo in cui si applica questo concetto di contenitori e valori nei contenitori. Ad esempio, un array è solo un elenco di cose. Un elenco espresso nel tempo è un flusso, quindi puoi applicare gli stessi tipi di utilità per elaborare flussi di eventi in arrivo, qualcosa che vedrai molto quando inizi a creare software reale con FP.

Dichiarativo vs Imperativo

La programmazione funzionale è un paradigma dichiarativo, nel senso che la logica del programma viene espressa senza descrivere esplicitamente il controllo del flusso.

I programmi imperativi spendono linee di codice che descrivono i passaggi specifici utilizzati per ottenere i risultati desiderati: il controllo del flusso: come fare le cose.

I programmi dichiarativi astraggono il processo di controllo del flusso e spendono invece righe di codice che descrivono il flusso di dati: cosa fare. Il modo in cui viene sottratto.

Ad esempio, questa mappatura imperativa prende una matrice di numeri e restituisce una nuova matrice con ogni numero moltiplicato per 2:

Questa mappatura dichiarativa fa la stessa cosa, ma estrae il controllo del flusso usando l'utilità funzionale Array.prototype.map (), che consente di esprimere più chiaramente il flusso di dati:

Il codice imperativo utilizza spesso le istruzioni. Un'istruzione è un pezzo di codice che esegue alcune azioni. Esempi di dichiarazioni comunemente usate includono, per, passare, lanciare, ecc ...

Il codice dichiarativo si basa maggiormente sulle espressioni. Un'espressione è un pezzo di codice che valuta un valore. Le espressioni sono generalmente una combinazione di chiamate di funzione, valori e operatori che vengono valutati per produrre il valore risultante.

Questi sono tutti esempi di espressioni:

2 * 2
doubleMap ([2, 3, 4])
Math.max (4, 3, 2)

Solitamente nel codice, vedrai le espressioni assegnate a un identificatore, restituite da funzioni o passate a una funzione. Prima di essere assegnato, restituito o passato, l'espressione viene prima valutata e viene utilizzato il valore risultante.

Conclusione

Favorisce la programmazione funzionale:

  • Funzioni pure invece di stato condiviso ed effetti collaterali
  • Immutabilità su dati mutabili
  • Composizione delle funzioni tramite controllo del flusso imperativo
  • Molte utility generiche e riutilizzabili che utilizzano funzioni di ordine superiore per agire su molti tipi di dati anziché su metodi che operano solo sui loro dati associati
  • Codice dichiarativo piuttosto che imperativo (cosa fare, piuttosto che come farlo)
  • Espressioni su dichiarazioni
  • Contenitori e funzioni di ordine superiore rispetto al polimorfismo ad hoc

Compiti a casa

Impara e pratica questo gruppo principale di extra funzionali di array:

  • .carta geografica()
  • .filtro()
  • .ridurre()

Usa map per trasformare il seguente array di valori in un array di nomi di elementi:

Usa il filtro per selezionare gli elementi in cui i punti sono maggiori o uguali a 3:

Usa riduci per sommare i punti:

Esplora la serie

  • Che cos'è una chiusura?
  • Qual è la differenza tra classe e ereditarietà prototipale?
  • Che cos'è una funzione pura?
  • Cos'è la composizione delle funzioni?
  • Cos'è la programmazione funzionale?
  • Che cos'è una promessa?
  • Competenze trasversali
Questo post è stato incluso nel libro "Composing Software".
Acquista il libro | Indice |
Inizia la tua lezione gratuita su EricElliottJS.com

Eric Elliott è l'autore dei libri, "Software di composizione" e "Programmazione di applicazioni JavaScript". Come co-fondatore di EricElliottJS.com e DevAnywhere.io, insegna agli sviluppatori le abilità di sviluppo software essenziali. Crea e consiglia team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall StreetJournal, ESPN, BBC e i migliori artisti discografici tra cui Usher, Frank Ocean, Metallica e molti altri.

Gode ​​di uno stile di vita remoto con la donna più bella del mondo.