Padroneggia l'intervista a JavaScript: che cos'è la composizione delle funzioni?

Google Datacenter Pipes - Jorge Jorquera - (CC-BY-NC-ND-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 sta conquistando il 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 composizione delle funzioni è il processo di combinazione di due o più funzioni per produrre una nuova funzione. Comporre insieme le funzioni è come collegare insieme una serie di pipe per far fluire i nostri dati.

In parole povere, una composizione di funzioni `f` e` g` può essere definita come `f (g (x))`, che valuta dall'interno verso l'esterno, da destra a sinistra. In altre parole, l'ordine di valutazione è:

  1. `x`
  2. `G`
  3. `f`

Diamo un'occhiata più da vicino nel codice. Immagina di voler convertire i nomi completi degli utenti in slug URL per dare a ciascuno dei tuoi utenti una pagina del profilo. Per fare ciò, è necessario eseguire una serie di passaggi:

  1. dividere il nome in un array su spazi
  2. mappare il nome in minuscolo
  3. unisciti con trattini
  4. codificare il componente URI

Ecco una semplice implementazione:

Non male ... ma se ti dicessi che potrebbe essere più leggibile?

Immagina che ognuna di queste operazioni avesse una corrispondente funzione componibile. Questo potrebbe essere scritto come:

Sembra ancora più difficile da leggere rispetto al nostro primo tentativo, ma resisti, questo sta andando da qualche parte.

Per raggiungere questo obiettivo, stiamo usando forme componibili di utilità comuni come `split ()`, `join ()` e `map ()`. Ecco le implementazioni:

Ad eccezione di `toLowerCase ()`, le versioni testate in produzione di tutte queste funzioni sono disponibili da Lodash / fp. Puoi importarli in questo modo:

import {curry, map, join, split} da 'lodash / fp';

O così:

const curry = request ('lodash / fp / curry');
const map = request ('lodash / fp / map');
// ...

Sono un po 'pigro qui. Si noti che questo curry non è tecnicamente un vero curry, che produrrebbe sempre una funzione unaria. Invece, è una semplice applicazione parziale. Vedi "Qual è la differenza tra curry e applicazione parziale?", Ma ai fini di questa dimostrazione, funzionerà in modo intercambiabile con una vera funzione al curry.

Tornando alla nostra implementazione `toSlug ()`, c'è qualcosa che mi dà davvero fastidio:

Mi sembra un sacco di nidificazione, ed è un po 'confuso da leggere. Possiamo appiattire l'annidamento con una funzione che comporrà automaticamente queste funzioni per noi, il che significa che prenderà l'output da una funzione e lo collegherà automaticamente all'ingresso della funzione successiva fino a quando non sputa il valore finale.

Vieni a pensarci bene, abbiamo un programma di utilità extra che sembra fare qualcosa del genere. Prende un elenco di valori e applica una funzione a ciascuno di questi valori, accumulando un singolo risultato. I valori stessi possono essere funzioni. La funzione si chiama `ridurre ()`, ma per abbinare il comportamento di composizione sopra, ne abbiamo bisogno per ridurre da destra a sinistra, anziché da sinistra a destra.

Per fortuna c'è un `reduceRight ()` che fa esattamente quello che stiamo cercando:

Come `.reduce ()`, l'array `.reduceRight ()` usa una funzione di riduzione e un valore iniziale (`x`). Esaminiamo le funzioni dell'array (da destra a sinistra), applicando ciascuna a sua volta al valore accumulato (`v`).

Con compose, possiamo riscrivere la nostra composizione sopra senza l'annidamento:

Naturalmente, `compose ()` viene fornito anche con lodash / fp:

import {compose} da 'lodash / fp';

O:

const compose = request ('lodash / fp / compose');

La composizione è fantastica quando stai pensando in termini di forma matematica di composizione, dentro e fuori ... ma cosa succede se vuoi pensare in termini di sequenza da sinistra a destra?

C'è un'altra forma comunemente chiamata `pipe ()`. Lodash lo chiama `flow ()`:

Nota che l'implementazione è esattamente la stessa di `compose ()`, tranne per il fatto che stiamo usando `.reduce ()` invece di `.reduceRight ()`, che riduce da sinistra a destra invece che da destra a sinistra.

Diamo un'occhiata alla nostra funzione `toSlug ()` implementata con `pipe ()`:

Per me, questo è molto più facile da leggere.

I programmatori funzionali hardcore definiscono la loro intera applicazione in termini di composizioni di funzioni. Lo uso spesso per eliminare la necessità di variabili temporanee. Guarda attentamente la versione `pipe ()` di `toSlug ()` e potresti notare qualcosa di speciale.

Nella programmazione imperativa, quando esegui trasformazioni su una variabile, troverai riferimenti alla variabile in ogni passaggio della trasformazione. L'implementazione `pipe ()` sopra è scritta in uno stile privo di punti, il che significa che non identifica gli argomenti su cui opera affatto.

Uso spesso pipe in cose come unit test e riduttori di stato Redux per eliminare la necessità di variabili intermedie che esistono solo per contenere valori transitori tra un'operazione e la successiva.

All'inizio può sembrare strano, ma man mano che ti eserciti con esso, scoprirai che nella programmazione funzionale, stai lavorando con funzioni molto astratte e generalizzate in cui i nomi delle cose non contano molto. I nomi si intromettono. Potresti iniziare a pensare alle variabili come a una caldaia non necessaria.

Detto questo, sono dell'opinione che lo stile senza punti possa essere portato troppo lontano. Può diventare troppo denso e difficile da capire, ma se ti confondi, ecco un piccolo suggerimento ... puoi attingere al flusso per tracciare ciò che sta succedendo:

Ecco come lo usi:

`trace ()` è solo una forma speciale del più generale `tap ()`, che consente di eseguire alcune azioni per ciascun valore che scorre attraverso la pipe. Prendilo? Tubo? Rubinetto? Puoi scrivere `tap ()` in questo modo:

Ora puoi vedere come `trace ()` sia solo un `tap ()` con un caso speciale:

Dovresti iniziare a capire com'è la programmazione funzionale e come l'applicazione parziale e il curry collaborano con la composizione delle funzioni per aiutarti a scrivere programmi più leggibili con meno piatti.

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

Aumenta di livello le tue abilità

Scopri JavaScript con Eric Elliott. Se non sei un membro, ti stai perdendo!

Eric Elliott è l'autore di "Programmazione di applicazioni JavaScript" (O’Reilly) e di un curriculum avanzato di JavaScript e sviluppo degli sviluppatori. Ha contribuito alle esperienze software per Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC e i migliori artisti della registrazione tra cui Usher, Frank Ocean, Metallica e molti altri.

Lavora dove vuole con la donna più bella del mondo.