Come posso _ leggere_ il codice JavaScript funzionale?


9

Credo di aver appreso alcuni / molti / molti dei concetti di base alla base della programmazione funzionale in JavaScript. Tuttavia, ho difficoltà a leggere in modo specifico il codice funzionale, anche il codice che ho scritto, e mi chiedo se qualcuno può darmi suggerimenti, suggerimenti, migliori pratiche, terminologia, ecc. Che possono aiutare.

Prendi il codice qui sotto. Ho scritto questo codice Ha lo scopo di assegnare una percentuale di somiglianza tra due oggetti, tra dire {a:1, b:2, c:3, d:3}e {a:1, b:1, e:2, f:2, g:3, h:5}. Ho prodotto il codice in risposta a questa domanda su StackTranslate.it . Poiché non ero sicuro del tipo di somiglianza percentuale richiesta dal poster, ho fornito quattro diversi tipi:

  • la percentuale di chiavi nel 1o oggetto che si trova nel 2o,
  • la percentuale dei valori nel 1 ° oggetto che è possibile trovare nel 2 °, inclusi i duplicati,
  • la percentuale dei valori nel 1 ° oggetto che è possibile trovare nel 2 °, senza duplicati consentiti, e
  • la percentuale di {chiave: valore} si accoppia nel primo oggetto che si trova nel secondo oggetto.

Ho iniziato con un codice ragionevolmente imperativo, ma ho subito capito che si trattava di un problema adatto alla programmazione funzionale. In particolare, mi sono reso conto che se avessi potuto estrarre una o tre funzioni per ognuna delle quattro strategie di cui sopra che definivano il tipo di funzione che stavo cercando di confrontare (ad esempio le chiavi, i valori, ecc.), Allora potrei essere in grado di ridurre (perdona il gioco di parole) il resto del codice in unità ripetibili. Sai, mantenerlo ASCIUTTO. Quindi sono passato alla programmazione funzionale. Sono abbastanza orgoglioso del risultato, penso che sia ragionevolmente elegante e penso di aver capito cosa ho fatto abbastanza bene.

Tuttavia, anche dopo aver scritto il codice da solo e averne compreso ogni parte durante la costruzione, quando ora guardo indietro su di esso, continuo a essere più che un po 'confuso sia su come leggere una particolare mezza linea, sia su come "grok" ciò che sta facendo una particolare riga di codice. Mi ritrovo a creare frecce mentali per collegare diverse parti che si degradano rapidamente in un pasticcio di spaghetti.

Quindi, qualcuno può dirmi come "leggere" alcuni dei pezzi di codice più contorti in un modo che sia allo stesso tempo conciso e che contribuisce alla mia comprensione di ciò che sto leggendo? Immagino che le parti che mi ottengono di più siano quelle che hanno più frecce grosse in una riga e / o parti che hanno più parentesi in una riga. Anche in questo caso, alla fine, riesco a capire la logica, ma (spero) c'è un modo migliore per andare avanti rapidamente e chiaramente "prendendo" una linea di programmazione JavaScript funzionale.

Sentiti libero di usare qualsiasi riga di codice dal basso o anche altri esempi. Tuttavia, se vuoi alcuni suggerimenti iniziali da me, eccone alcuni. Inizia con una ragionevolmente semplice. Da vicino la fine del codice, c'è questa che viene passato come parametro a una funzione: obj => key => obj[key]. Come si fa a leggerlo e capirlo? Un esempio è più una funzione completa dal prossimità della partenza: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. L'ultima mapparte mi prende in particolare.

Si prega di notare, a questo punto nel tempo che sto , non alla ricerca di riferimenti a Haskell o simbolico notazione astratta o fondamenti della accattivarsi, ecc Quello che sto cercando è frasi in inglese che posso silenziosamente bocca mentre guardando una riga di codice. Se hai riferimenti che si rivolgono esattamente a questo, fantastico, ma non cerco anche risposte che dicano che dovrei andare a leggere alcuni libri di testo di base. L'ho fatto e ottengo (almeno una quantità significativa di) la logica. Inoltre, non ho bisogno di risposte esaustive (anche se tali tentativi sarebbero i benvenuti): anche le risposte brevi che forniscono un modo elegante di leggere una singola riga particolare di codice altrimenti problematico sarebbero apprezzate.

Suppongo che una parte di questa domanda sia: posso persino leggere il codice funzionale in modo lineare, da sinistra a destra e dall'alto verso il basso? O è una più o meno costretti a creare un'immagine mentale di spaghetti-come il cablaggio sulla pagina di codice che è decisamente non lineare? E se uno deve farlo, dobbiamo ancora leggere il codice, quindi come possiamo prendere un testo lineare e collegare gli spaghetti?

Eventuali suggerimenti sarebbero apprezzati.

const obj1 = { a:1, b:2, c:3, d:3 };
const obj2 = { a:1, b:1, e:2, f:2, g:3, h:5 };

// x or X is key or value or key/value pair

const getXs = (obj, getX) =>
  Object.keys(obj).map(key => getX(obj)(key));

const getPctSameXs = (getX, filter = vals => vals) =>
  (objA, objB) =>
    filter(getXs(objB, getX))
      .reduce(
        (numSame, x) =>
          getXs(objA, getX).indexOf(x) > -1 ? numSame + 1 : numSame,
        0
      ) / Object.keys(objA).length * 100;

const pctSameKeys       = getPctSameXs(obj => key => key);
const pctSameValsDups   = getPctSameXs(obj => key => obj[key]);
const pctSameValsNoDups = getPctSameXs(obj => key => obj[key], vals => [...new Set(vals)]);
const pctSameProps      = getPctSameXs(obj => key => JSON.stringify( {[key]: obj[key]} ));

console.log('obj1:', JSON.stringify(obj1));
console.log('obj2:', JSON.stringify(obj2));
console.log('% same keys:                   ', pctSameKeys      (obj1, obj2));
console.log('% same values, incl duplicates:', pctSameValsDups  (obj1, obj2));
console.log('% same values, no duplicates:  ', pctSameValsNoDups(obj1, obj2));
console.log('% same properties (k/v pairs): ', pctSameProps     (obj1, obj2));

// output:
// obj1: {"a":1,"b":2,"c":3,"d":3}
// obj2: {"a":1,"b":1,"e":2,"f":2,"g":3,"h":5}
// % same keys:                    50
// % same values, incl duplicates: 125
// % same values, no duplicates:   75
// % same properties (k/v pairs):  25

Risposte:


18

Per lo più hai difficoltà a leggerlo perché questo esempio particolare non è molto leggibile. Senza offesa, neanche una proporzione scoraggiante di campioni che trovi su Internet. Molte persone giocano solo con la programmazione funzionale nei fine settimana e non devono mai occuparsi di mantenere il codice funzionale di produzione a lungo termine. Lo scriverei più così:

function mapObj(obj, f) {
  return Object.keys(obj).map(key => f(obj, key));
}

function getPctSameXs(obj1, obj2, f) {
  const mapped1 = mapObj(obj1, f);
  const mapped2 = mapObj(obj2, f);
  const same = mapped1.filter(x => mapped2.indexOf(x) != -1);
  const percent = same.length / mapped1.length * 100;
  return percent;
}

const getValues = (obj, key) => obj[key];
const valuesWithDupsPercent = getPctSameXs(obj1, obj2, getValues);

Per qualche ragione molte persone hanno in testa questa idea che il codice funzionale dovrebbe avere un certo "aspetto" estetico di una grande espressione nidificata. Nota che sebbene la mia versione assomigli in qualche modo al codice imperativo con tutti i punti e virgola, tutto è immutabile, quindi potresti sostituire tutte le variabili e ottenere una grande espressione se lo desideri. È davvero "funzionale" come la versione spaghetti, ma con più leggibilità.

Qui le espressioni sono suddivise in pezzi molto piccoli e dati nomi che sono significativi per il dominio. La nidificazione viene evitata inserendo funzionalità comuni come mapObjin una funzione denominata. Le lambda sono riservate a funzioni molto brevi con uno scopo chiaro nel contesto.

Se ti imbatti in un codice difficile da leggere, esegui il refactoring fino a quando non sarà più facile. Ci vuole un po 'di pratica, ma vale la pena. Il codice funzionale può essere leggibile tanto quanto l'imperativo. In realtà, spesso moreso, perché di solito è più conciso.


Sicuramente senza offesa! Mentre continuerò a sostenere che conosco alcune cose sulla programmazione funzionale, forse le mie affermazioni nella domanda su quanto so fossero un po 'troppo enunciate. Sono davvero un principiante relativo. Quindi, vedendo come questo mio particolare tentativo possa essere riscritto in un modo così conciso, chiaro ma ancora funzionale, sembra oro ... grazie. Studierò attentamente la tua riscrittura.
Andrew Willems,

1
Ho sentito dire che avere lunghe catene e / o annidamento di metodi elimina le variabili intermedie non necessarie. Al contrario, la tua risposta rompe le mie catene / annidamento in istruzioni indipendenti intermedie utilizzando variabili intermedie ben denominate. Trovo il tuo codice più leggibile in questo caso, ma mi chiedo quanto generale stai cercando di essere. Stai dicendo che lunghe catene di metodi e / o annidamenti profondi sono spesso o addirittura sempre un anti-pattern da evitare, o ci sono momenti in cui portano benefici significativi? E la risposta a questa domanda è diversa per la codifica funzionale rispetto a quella imperativa?
Andrew Willems,

3
Ci sono alcune situazioni in cui l'eliminazione delle variabili intermedie può aggiungere chiarezza. Ad esempio, in FP non si desidera quasi mai un indice in un array. Inoltre a volte non esiste un nome eccezionale per il risultato intermedio. Nella mia esperienza, tuttavia, la maggior parte delle persone tende a sbagliare troppo dall'altra parte.
Karl Bielefeldt,

6

Non ho fatto molto lavoro altamente funzionale in Javascript (il che direi che lo è - la maggior parte delle persone che parlano di Javascript funzionale potrebbero usare mappe, filtri e riduzioni, ma il tuo codice definisce le sue funzioni di livello superiore , che è un po 'più avanzato di così), ma l'ho fatto in Haskell e penso che almeno una parte dell'esperienza si traduca. Ti darò alcuni suggerimenti per le cose che ho imparato:

Specificare i tipi di funzioni è davvero importante. Haskell non richiede di specificare quale sia il tipo di una funzione, ma includere il tipo nella definizione rende molto più facile la lettura. Sebbene Javascript non supporti la digitazione esplicita allo stesso modo, non c'è motivo di non includere la definizione del tipo in un commento, ad esempio:

// getXs :: forall O, F . O -> (O -> String -> F) -> [F]
const getXs = (obj, getX) =>
    Object.keys(obj).map(key => getX(obj)(key));

Con un po 'di pratica nel lavorare con definizioni di tipo come questa, rendono il significato di una funzione molto più chiaro.

La denominazione è importante, forse ancor più che nella programmazione procedurale. Molti programmi funzionali sono scritti in uno stile molto conciso che è pesante sulla convenzione (ad esempio la convenzione che "xs" è un elenco / array e che "x" è un elemento in esso è molto pervasiva), ma a meno che tu non capisca quello stile facilmente suggerirei una denominazione più dettagliata. Guardando i nomi specifici che hai usato, "getX" è piuttosto opaco, e quindi anche "getXs" non aiuta molto. Definirei "getXs" come "applyToProperties" e "getX" sarebbe probabilmente "propertyMapper". "getPctSameXs" sarebbe quindi "percentPropertiesSameWith" ("con").

Un'altra cosa importante è scrivere codice idiomatico . Ho notato che stai usando una sintassi a => b => some-expression-involving-a-and-bper produrre funzioni al curry. Questo è interessante e potrebbe essere utile in alcune situazioni, ma qui non stai facendo nulla che tragga vantaggio dalle funzioni curry e sarebbe invece più idiomatico Javascript utilizzare invece le tradizionali funzioni a argomento multiplo. In questo modo potrebbe essere più semplice vedere cosa sta succedendo a colpo d'occhio. Stai anche usando const name = lambda-expressionper definire le funzioni, dove invece sarebbe più idiomatico da usare function name (args) { ... }. So che sono semanticamente leggermente diversi, ma a meno che tu non faccia affidamento su tali differenze, suggerirei di utilizzare la variante più comune quando possibile.


5
+1 per i tipi! Solo perché la lingua non li ha, non significa che non devi pensarci . Diversi sistemi di documentazione per ECMAScript hanno un linguaggio di tipo per la registrazione dei tipi di funzioni. Numerosi IDE ECMAScript hanno anche un linguaggio di tipo (e di solito comprendono anche i linguaggi di tipo per i principali sistemi di documentazione) e possono persino eseguire un controllo rudimentale del tipo e un suggerimento euristico usando tali annotazioni di tipo .
Jörg W Mittag,

Mi hai dato molto da masticare: definizioni di tipo, nomi significativi, usando modi di dire ... grazie! Solo alcuni dei molti commenti possibili: non intendevo necessariamente scrivere alcune parti come funzioni al curry; si sono semplicemente evoluti in quel modo mentre rifattavo il mio codice durante la scrittura. Vedo ora come non fosse necessario e anche solo unendo i parametri di quelle due funzioni in due parametri per una singola funzione non solo ha più senso, ma rende immediatamente quel breve bit almeno più leggibile.
Andrew Willems,

@ JörgWMittag, grazie per i tuoi commenti sull'importanza dei tipi e per il link a quell'altra risposta che hai scritto. Uso WebStorm e non mi sono reso conto che, secondo come ho letto l'altra tua risposta, WebStorm sa come interpretare le annotazioni simili a jsdoc. Presumo dal tuo commento che jsdoc e WebStorm possano essere usati insieme per annotare il codice funzionale, non solo imperativo, ma dovrei approfondire ulteriormente per saperlo davvero. Ho giocato con jsdoc prima e ora che so che WebStorm e posso collaborare lì, mi aspetto che userò quella caratteristica / approccio di più.
Andrew Willems,

@Jules, solo per chiarire a quale funzione curry mi riferivo nel mio commento sopra: Come hai sottinteso, ogni istanza di obj => key => ...può essere semplificata (obj, key) => ...perché in seguito getX(obj)(key)può anche essere semplificata get(obj, key). Al contrario, un'altra funzione al curry, (getX, filter = vals => vals) => (objA, objB) => ...non può essere facilmente semplificata, almeno nel contesto del resto del codice come scritto.
Andrew Willems,
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.