Introduzione di un programmatore funzionale a JavaScript (software di composizione)

Smoke Art Cubes to Smoke - MattysFlicks - (CC BY 2.0)
Nota: fa parte della serie "Composing Software" (ora un libro!) Sull'apprendimento da zero della programmazione funzionale e delle tecniche software compositive in JavaScriptES6 +. Rimanete sintonizzati. C'è molto di più a venire!
Acquista il libro | Indice |

Per coloro che non hanno familiarità con JavaScript o ES6 +, questo è inteso come una breve introduzione. Che tu sia uno sviluppatore JavaScript principiante o esperto, potresti imparare qualcosa di nuovo. Quello che segue ha solo lo scopo di graffiare la superficie e farti eccitare. Se vuoi saperne di più, dovrai solo esplorare più a fondo. C'è molto di più avanti.

Il modo migliore per imparare a programmare è programmare. Ti consiglio di seguire insieme un ambiente di programmazione JavaScript interattivo come CodePen o Babel REPL.

In alternativa, è possibile cavarsela utilizzando le REPL della console del nodo o del browser.

Espressioni e valori

Un'espressione è una porzione di codice che restituisce un valore.

Le seguenti sono tutte espressioni valide in JavaScript:

7;
7 + 1; // 8
7 * 2; // 14
'Ciao'; // Ciao

Al valore di un'espressione può essere assegnato un nome. Quando lo fai, l'espressione viene valutata per prima e il valore risultante viene assegnato al nome. Per questo, useremo la parola chiave const. Non è l'unico modo, ma è quello che utilizzerai di più, quindi per ora continueremo con const:

const hello = 'Hello';
Ciao; // Ciao

var, let e const

JavaScript supporta altre due parole chiave con dichiarazione variabile: var e let. Mi piace pensarli in termini di ordine di selezione. Per impostazione predefinita, seleziono la dichiarazione più rigorosa: const. Una variabile dichiarata con la parola chiave const non può essere riassegnata. Il valore finale deve essere assegnato al momento della dichiarazione. Questo può sembrare rigido, ma la restrizione è una buona cosa. È un segnale che ti dice "il valore assegnato a questo nome non cambierà". Ti aiuta a comprendere appieno cosa significa il nome immediatamente, senza bisogno di leggere l'intera funzione o bloccare l'ambito.

A volte è utile riassegnare le variabili. Ad esempio, se stai utilizzando l'iterazione manuale e imperativa anziché un approccio più funzionale, puoi iterare un contatore assegnato con let.

Poiché var ti dice il minimo della variabile, è il segnale più debole. Da quando ho iniziato a utilizzare ES6, non ho mai dichiarato intenzionalmente una var in un vero progetto software.

Tenere presente che una volta che una variabile viene dichiarata con let o const, qualsiasi tentativo di dichiararla nuovamente comporterà un errore. Se si preferisce una maggiore flessibilità sperimentale nell'ambiente REPL (Read, Eval, Print Loop), è possibile utilizzare var anziché const per dichiarare le variabili. La redeclaring var è consentita.

Questo testo userà const per farti prendere l'abitudine di default a const per programmi reali, ma sentiti libero di sostituire var allo scopo della sperimentazione interattiva.

tipi

Finora abbiamo visto due tipi: numeri e stringhe. JavaScript ha anche valori booleani (veri o falsi), array, oggetti e altro. Passeremo ad altri tipi più tardi.

Un array è un elenco ordinato di valori. Pensalo come una scatola che può contenere molti oggetti. Ecco la notazione letterale dell'array:

[1, 2, 3];

Naturalmente, questa è un'espressione a cui può essere assegnato un nome:

const arr = [1, 2, 3];

Un oggetto in JavaScript è una raccolta di coppie chiave: valore. Ha anche una notazione letterale:

{
  chiave: 'valore'
}

E, naturalmente, puoi assegnare un oggetto a un nome:

const foo = {
  bar: 'bar'
}

Se si desidera assegnare variabili esistenti a oggetti proprietà chiavi con lo stesso nome, esiste una scorciatoia per quello. Puoi semplicemente digitare il nome della variabile invece di fornire sia una chiave che un valore:

const a = 'a';
const oldA = {a: a}; // modo lungo e ridondante
const oA = {a}; // insomma un dolce!

Solo per divertimento, facciamolo di nuovo:

const b = 'b';
const oB = {b};

Gli oggetti possono essere facilmente composti insieme in nuovi oggetti:

const c = {... oA, ... oB}; // {a: 'a', b: 'b'}

Quei punti sono l'operatore di diffusione dell'oggetto. Esegue un'iterazione sulle proprietà in oA e le assegna al nuovo oggetto, quindi fa lo stesso per oB, sovrascrivendo tutte le chiavi già esistenti sul nuovo oggetto. Al momento della stesura di questo articolo, la diffusione degli oggetti è una nuova funzionalità sperimentale che potrebbe non essere ancora disponibile in tutti i browser più diffusi, ma se non funziona per te, esiste un sostituto: Object.assign ():

const d = Object.assign ({}, oA, oB); // {a: 'a', b: 'b'}

Solo un po 'più di digitazione nell'esempio Object.assign (), e se stai componendo molti oggetti, potresti persino salvarti un po' di battitura. Si noti che quando si utilizza Object.assign (), è necessario passare un oggetto di destinazione come primo parametro. È l'oggetto in cui verranno copiate le proprietà. Se si dimentica e si omette l'oggetto di destinazione, l'oggetto che si passa nel primo argomento verrà modificato.

Nella mia esperienza, mutare un oggetto esistente piuttosto che creare un nuovo oggetto è di solito un bug. Per lo meno, è soggetto a errori. Fai attenzione con Object.assign ().

destrutturazione

Sia gli oggetti che le matrici supportano la destrutturazione, il che significa che è possibile estrarre valori da essi e assegnarli a variabili denominate:

const [t, u] = ['a', 'b'];
t; // 'a'
u; // 'b'
const blep = {
  blop: 'blop'
};

// Quanto segue equivale a:
// const blop = blep.blop;
const {blop} = blep;
Blop; // 'blop'

Come nell'esempio di matrice sopra, è possibile destrutturare in più assegnazioni contemporaneamente. Ecco una linea che vedrai in molti progetti Redux:

const {tipo, payload} = azione;

Ecco come viene utilizzato nel contesto di un riduttore (molto di più su questo argomento che verrà dopo):

const myReducer = (state = {}, action = {}) => {
  const {tipo, payload} = azione;
  switch (tipo) {
    case 'FOO': return Object.assign ({}, stato, payload);
    impostazione predefinita: stato di ritorno;
  }
};

Se non desideri utilizzare un nome diverso per il nuovo binding, puoi assegnare un nuovo nome:

const {blop: bloop} = blep;
Bloop; // 'blop'

Leggi: assegna blep.blop come bloop.

Confronti e ternari

Puoi confrontare i valori con l'operatore di uguaglianza rigorosa (a volte chiamato "triple egals"):

3 + 1 === 4; // vero

C'è anche un operatore di uguaglianza sciatta. È formalmente noto come operatore "uguale". Informalmente, "doppio uguale". Il doppio uguale ha un caso d'uso valido o due, ma è quasi sempre preferibile impostare automaticamente l'operatore ===.

Altri operatori di confronto includono:

  • > Maggiore di
  • > = Maggiore o uguale a
  • <= Minore o uguale a
  • ! = Non uguale
  • ! == Non uguale severo
  • && Logical e
  • || Logico o

Un'espressione ternaria è un'espressione che ti consente di porre una domanda utilizzando un comparatore e valuta una risposta diversa a seconda che l'espressione sia vera o meno:

14 - 7 === 7? 'Sì!' : 'No.'; // Sì!

funzioni

JavaScript ha espressioni di funzione che possono essere assegnate ai nomi:

const double = x => x * 2;

Ciò significa la stessa cosa della funzione matematica f (x) = 2x. Parlato ad alta voce, quella funzione legge f di x equivale a 2x. Questa funzione è interessante solo quando la applichi a un valore specifico di x. Per usare la funzione in altre equazioni, dovresti scrivere f (2), che ha lo stesso significato di 4.

In altre parole, f (2) = 4. Puoi pensare a una funzione matematica come una mappatura da input a output. f (x) in questo caso è una mappatura dei valori di input per x ai corrispondenti valori di output pari al prodotto del valore di input e 2.

In JavaScript, il valore di un'espressione di funzione è la funzione stessa:

Doppio; // [Funzione: doppio]

Puoi vedere la definizione della funzione usando il metodo .toString ():

double.toString (); // 'x => x * 2'

Se si desidera applicare una funzione ad alcuni argomenti, è necessario richiamarla con una chiamata di funzione. Una chiamata di funzione applica una funzione ai suoi argomenti e restituisce un valore di ritorno.

È possibile richiamare una funzione utilizzando (argomento1, argomento2, ... resto). Ad esempio, per invocare la nostra doppia funzione, basta aggiungere le parentesi e passare un valore per raddoppiare:

doppio (2); // 4

A differenza di alcuni linguaggi funzionali, queste parentesi sono significative. Senza di essi, la funzione non può essere chiamata:

doppio 4; // SyntaxError: numero imprevisto

firme

Le funzioni hanno firme, che consistono in:

  1. Un nome funzione opzionale.
  2. Un elenco di tipi di parametri, tra parentesi. I parametri possono essere facoltativamente nominati.
  3. Il tipo del valore restituito.

Non è necessario specificare le firme di tipo in JavaScript. Il motore JavaScript capirà i tipi in fase di esecuzione. Se fornisci abbastanza indizi, la firma può essere dedotta anche da strumenti di sviluppo come IDE (Integrated Development Environment) e Tern.js utilizzando l'analisi del flusso di dati.

JavaScript manca della propria notazione della firma della funzione, quindi ci sono alcuni standard concorrenti: JSDoc è stato storicamente molto popolare, ma è goffamente dettagliato, e nessuno si preoccupa di mantenere aggiornati i commenti dei documenti con il codice, così molti sviluppatori JS hanno smesso di usarlo.

TypeScript e Flow sono attualmente i principali contendenti. Non sono sicuro di come esprimere tutto ciò di cui ho bisogno in nessuno dei due, quindi uso Rtype solo a scopo di documentazione. Alcune persone ricadono sui tipi Hindley – Milner solo al curry di Haskell. Mi piacerebbe vedere un buon sistema di notazione standardizzato per JavaScript, anche solo a scopo di documentazione, ma al momento non credo che nessuna delle soluzioni attuali sia all'altezza del compito. Per ora, strizza gli occhi e fai del tuo meglio per tenere il passo con le strane firme di tipo che probabilmente sembrano leggermente diverse da qualsiasi cosa tu stia usando.

functionName (param1: Type, param2: Type) => Type

La firma per il doppio è:

double (x: n) => Numero

Nonostante il fatto che JavaScript non richieda l'annotazione delle firme, sapere quali sono le firme e cosa significano sarà comunque importante per comunicare in modo efficiente su come vengono utilizzate le funzioni e su come sono composte le funzioni. La maggior parte delle utility di composizione delle funzioni riutilizzabili richiedono di passare funzioni che condividono la stessa firma di tipo.

Valori dei parametri predefiniti

JavaScript supporta i valori dei parametri predefiniti. La seguente funzione funziona come una funzione di identità (una funzione che restituisce lo stesso valore che passi), a meno che non la chiami con indefinito o semplicemente non passi alcun argomento, quindi restituisce zero, invece:

const orZero = (n = 0) => n;

Per impostare un valore predefinito, è sufficiente assegnarlo al parametro con l'operatore = nella firma della funzione, come in n = 0, sopra. Quando si assegnano i valori predefiniti in questo modo, strumenti di inferenza del tipo come Tern.js, Flow o TypeScript possono inferire automaticamente la firma del tipo della funzione, anche se non si dichiarano esplicitamente le annotazioni del tipo.

Il risultato è che, con i plug-in corretti installati nel tuo editor o IDE, sarai in grado di vedere le firme delle funzioni visualizzate in linea mentre digiti le chiamate di funzione. Sarai anche in grado di capire come utilizzare una funzione a colpo d'occhio in base alla sua firma di chiamata. L'uso di assegnazioni predefinite ovunque abbia senso può aiutarti a scrivere più codice autocompattante.

Nota: i parametri con valori predefiniti non contano ai fini della proprietà .length della funzione, che eliminerà utilità come autocurry che dipendono dal valore .length. Alcune utilità di curry (come lodash / curry) ti consentono di passare un'arità personalizzata per aggirare questa limitazione se ti imbatti in.

Argomenti nominati

Le funzioni JavaScript possono prendere letterali oggetto come argomenti e utilizzare l'assegnazione destrutturante nella firma del parametro per ottenere l'equivalente degli argomenti denominati. Nota, puoi anche assegnare valori di default ai parametri usando la funzione di parametro di default:

const createUser = ({
  name = 'Anonimo',
  avatarThumbnail = '/ / avatars/anonymous.png'
}) => ({
  nome,
  avatarThumbnail
});
const george = createUser ({
  nome: "George",
  avatarThumbnail: 'avatars / shades-emoji.png'
});
Giorgio;
/ *
{
  nome: "George",
  avatarThumbnail: 'avatars / shades-emoji.png'
}
* /

Riposa e diffondi

Una caratteristica comune delle funzioni in JavaScript è la possibilità di riunire un gruppo di argomenti rimanenti nella firma delle funzioni utilizzando l'operatore restante: ...

Ad esempio, la seguente funzione elimina semplicemente il primo argomento e restituisce il resto come un array:

const aTail = (testa, ... coda) => coda;
aTail (1, 2, 3); // [2, 3]

Rest riunisce i singoli elementi in un array. Diffusione fa il contrario: diffonde gli elementi da un array a singoli elementi. Considera questo:

const shiftToLast = (testa, ... coda) => [... coda, testa];
shiftToLast (1, 2, 3); // [2, 3, 1]

Le matrici in JavaScript hanno un iteratore che viene invocato quando viene utilizzato l'operatore di diffusione. Per ogni elemento dell'array, l'iteratore fornisce un valore. Nell'espressione, [... tail, head], l'iteratore copia ogni elemento in ordine dall'array tail nel nuovo array creato dalla notazione letterale circostante. Dato che la testa è già un singolo elemento, la facciamo semplicemente cadere alla fine dell'array e abbiamo finito.

accattivarsi

Una funzione curry è una funzione che accetta più parametri uno alla volta: accetta un parametro e restituisce una funzione che accetta il parametro successivo, e così via fino a quando non sono stati forniti tutti i parametri, a quel punto, l'applicazione è completata e il viene restituito il valore finale.

Il curry e l'applicazione parziale possono essere abilitati restituendo un'altra funzione:

const highpass = cutoff => n => n> = cutoff;
const gt4 = highpass (4); // highpass () restituisce una nuova funzione

Non è necessario utilizzare le funzioni freccia. JavaScript ha anche una parola chiave funzione. Stiamo usando le funzioni freccia perché la parola chiave funzione è molto più digitante. Ciò equivale alla definizione highPass (), sopra:

const highpass = funzione highpass (cutoff) {
  funzione di ritorno (n) {
    return n> = cutoff;
  };
};

La freccia in JavaScript significa approssimativamente "funzione". Ci sono alcune differenze importanti nel comportamento delle funzioni a seconda del tipo di funzione che usi (=> manca proprio questo, e non può essere usato come costruttore), ma arriveremo a queste differenze quando ci arriveremo. Per ora, quando vedi x => x, pensa "una funzione che prende x e restituisce x". Quindi puoi leggere const highpass = cutoff => n => n> = cutoff; come:

"Highpass è una funzione che accetta cutoff e restituisce una funzione che accetta n e restituisce il risultato di n> = cutoff".

Poiché highpass () restituisce una funzione, puoi usarla per creare una funzione più specializzata:

const gt4 = highpass (4);
gt4 (6); // vero
gt4 (3); // falso

Autocurry ti consente di eseguire automaticamente le funzioni di curry, per la massima flessibilità. Supponi di avere una funzione add3 ():

const add3 = curry ((a, b, c) => a + b + c);

Con autocurry, puoi usarlo in diversi modi e restituirà la cosa giusta a seconda di quanti argomenti passi:

add3 (1, 2, 3); // 6
add3 (1, 2) (3); // 6
add3 (1) (2, 3); // 6
add3 (1) (2) (3); // 6

Spiacente fan Haskell, JavaScript manca di un meccanismo di autocurry incorporato, ma è possibile importarne uno da Lodash:

$ npm install - salva lodash

Quindi, nei tuoi moduli:

importazione di curry da 'lodash / curry';

Oppure puoi usare il seguente incantesimo:

// Autocurry minuscolo e ricorsivo
const curry = (
  f, arr = []
) => (... args) => (
  a => a.length === f.length?
    fa) :
    curry (f, a)
) ([... arr, ... args]);

Composizione delle funzioni

Ovviamente puoi comporre funzioni. La composizione della funzione è il processo di passaggio del valore restituito da una funzione come argomento a un'altra funzione. In notazione matematica:

f. g

Il che si traduce in questo in JavaScript:

f (g (x))

È valutato dall'interno verso l'esterno:

  1. viene valutato x
  2. g () viene applicato a x
  3. f () viene applicato al valore restituito di g (x)

Per esempio:

const inc = n => n + 1;
inc (doppia (2)); // 5

Il valore 2 viene passato in double (), che produce 4. 4 viene passato in inc () che restituisce 5.

È possibile passare qualsiasi espressione come argomento a una funzione. L'espressione verrà valutata prima dell'applicazione della funzione:

inc (doppio (2) * doppio (2)); // 17

Poiché double (2) restituisce 4, è possibile leggerlo come inc (4 * 4) che restituisce inc (16) e quindi 17.

La composizione delle funzioni è fondamentale per la programmazione funzionale. Ne avremo molto di più in seguito.

Array

Le matrici hanno alcuni metodi integrati. Un metodo è una funzione associata a un oggetto: di solito una proprietà dell'oggetto associato:

const arr = [1, 2, 3];
arr.map (doppia); // [2, 4, 6]

In questo caso, arr è l'oggetto, .map () è una proprietà dell'oggetto con una funzione per un valore. Quando lo invochi, la funzione viene applicata agli argomenti, così come uno speciale parametro chiamato this, che viene impostato automaticamente quando viene invocato il metodo. Questo valore è il modo in cui .map () ottiene l'accesso al contenuto dell'array.

Tieni presente che stiamo trasferendo la doppia funzione come valore nella mappa anziché chiamarla. Questo perché map accetta una funzione come argomento e la applica a ciascun elemento dell'array. Restituisce un nuovo array contenente i valori restituiti da double ().

Si noti che il valore arr originale è invariato:

arr; // [1, 2, 3]

Metodo di concatenamento

Puoi anche concatenare chiamate di metodo. Il concatenamento di metodi è il processo di chiamata diretta di un metodo sul valore restituito di una funzione, senza la necessità di fare riferimento al valore restituito per nome:

const arr = [1, 2, 3];
arr.map (doppia) .map (doppia); // [4, 8, 12]

Un predicato è una funzione che restituisce un valore booleano (vero o falso). Il metodo .filter () accetta un predicato e restituisce un nuovo elenco, selezionando solo gli elementi che passano il predicato (return true) da includere nel nuovo elenco:

[2, 4, 6]. Filtro (gt4); // [4, 6]

Spesso, dovrai selezionare gli elementi da un elenco e quindi mapparli a un nuovo elenco:

[2, 4, 6] .filter (gt4) .map (doppio); [8, 12]

Nota: più avanti in questo testo, vedrai un modo più efficiente per selezionare e mappare allo stesso tempo usando qualcosa chiamato trasduttore, ma ci sono altre cose da esplorare prima.

Conclusione

Se la testa gira in questo momento, non preoccuparti. Abbiamo appena graffiato la superficie di molte cose che meritano molta più esplorazione e considerazione. Torneremo indietro ed esploreremo alcuni di questi argomenti in modo molto più approfondito, presto.

Acquista il libro | Indice |

Maggiori informazioni su EricElliottJS.com

Le lezioni video con sfide di codice interattivo sono disponibili per i membri di EricElliottJS.com. Se non sei un membro, iscriviti oggi.

Eric Elliott è un esperto di sistemi distribuiti e autore dei libri "Composing Software" e "Programmazione di applicazioni JavaScript". Come co-fondatore di DevAnywhere.io, insegna agli sviluppatori le competenze di cui hanno bisogno per lavorare in remoto e abbracciare l'equilibrio tra lavoro e vita privata. Crea e fornisce consulenza a team di sviluppo per progetti crittografici e ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall Street Journal, 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.