Come posso _read_ codice JavaScript funzionale?

9

Credo di aver imparato alcuni / molti / molti dei concetti di base alla base della programmazione funzionale in JavaScript. Tuttavia, ho problemi a leggere codice funzionale, persino il codice che ho scritto, e mi chiedo se qualcuno può darmi suggerimenti, consigli, best practice, terminologia, ecc. Che possano aiutare.

Prendi il codice qui sotto. Ho scritto questo codice. Mira ad assegnare una percentuale di somiglianza tra due oggetti, tra say {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 Stack Overflow . Poiché non ero sicuro di quale tipo di percentuale di somiglianza ci fosse nel poster, ho fornito quattro tipi diversi:

  • la percentuale delle chiavi nel primo oggetto che si trova nel 2 °
  • la percentuale dei valori nel primo oggetto che si trova nel 2 °, inclusi i duplicati,
  • la percentuale dei valori nel primo oggetto che si trova nel 2 °, senza duplicati consentiti, e
  • la percentuale di coppie {chiave: valore} nel primo oggetto che si trova nel 2 ° 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 potevo estrarre una funzione o tre per ognuna delle quattro strategie sopra descritte che definivano il tipo di funzionalità che stavo cercando di confrontare (es. Le chiavi, o i valori, ecc.), Allora potrei essere in grado di ridurre (perdonare il gioco di parole) il resto del codice in unità ripetibili. Sai, tenerlo ASCIUTTO. Così sono passato alla programmazione funzionale. Sono piuttosto orgoglioso del risultato, penso sia ragionevolmente elegante, e penso di capire cosa ho fatto abbastanza bene.

Tuttavia, pur avendo scritto il codice da solo e comprendendone ogni parte durante la costruzione, quando ora guardo indietro su di esso, continuo ad essere più che un po 'sconcertato sia a leggere qualsiasi particolare semiretta, sia come "ingannare" ciò che ogni particolare semiretta di codice sta effettivamente facendo. Mi trovo a fare frecce mentali per collegare diverse parti che si degradano rapidamente in un mucchio di spaghetti.

Quindi qualcuno può dirmi come "leggere" alcuni dei bit di codice più contorti in un modo che è sia conciso e che contribuisce alla mia comprensione di ciò che sto leggendo? Immagino che le parti che mi danno di più siano quelle che hanno più frecce grosse di fila e / o parti che hanno parecchie parentesi in fila. Di nuovo, al loro interno, posso finalmente capire la logica, ma (spero) ci sia un modo migliore per andare rapidamente e chiaramente e direttamente "prendendo in considerazione" una linea di programmazione JavaScript funzionale.

Sentiti libero di usare qualsiasi riga di codice dal basso, o anche altri esempi. Tuttavia, se vuoi qualche suggerimento iniziale da parte mia, eccone alcuni. Inizia con uno ragionevolmente semplice. Da vicino alla fine del codice, c'è questo che viene passato come parametro a una funzione: obj => key => obj[key] . Come si legge e si capisce? Un esempio più lungo è una funzione completa da vicino all'inizio: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key)); . L'ultima parte di% co_de mi fa in particolare.

Nota bene, al momento sono non in cerca di riferimenti a Haskell o alla notazione astratta simbolica o ai fondamenti del curry, ecc. Cosa sono em> cercare è frasi in inglese che posso tacere a bocca aperta mentre osservo una riga di codice. Se hai riferimenti specifici che rispondono esattamente a questo, ottimo, ma non sto cercando risposte che dicano che dovrei andare leggi alcuni libri di testo di base. L'ho fatto e ho ottenuto (almeno una parte significativa) la logica. Inoltre, non ho bisogno di risposte esaurienti (anche se tali tentativi sarebbero graditi): anche le risposte brevi che forniscono un modo elegante di leggere una singola riga di codice altrimenti problematico sarebbero apprezzate.

Suppongo che una parte di questa domanda sia: Può leggo anche il codice funzionale in modo lineare, sai, da sinistra a destra e dall'alto verso il basso? O è quasi obbligato a creare un'immagine mentale del cablaggio simile a spaghetti sulla pagina del codice che è decisamente non lineare? E se uno deve farlo, dobbiamo ancora leggere il codice, quindi come facciamo a prendere testo lineare e cablare gli spaghetti?

Qualche consiglio sarebbe apprezzato.

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
    
posta Andrew Willems 11.02.2017 - 03:04
fonte

2 risposte

18

Di solito hai difficoltà a leggerlo perché questo particolare esempio non è molto leggibile. Senza offesa intesa, una proporzione sconfortante di campioni che trovi su Internet non lo sono neanche. Un sacco di gente gioca solo con la programmazione funzionale nei fine settimana e non ha mai veramente a che fare con il mantenimento del codice funzionale di produzione a lungo termine. Lo scriverei più in questo modo:

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 motivo molte persone hanno questa idea in testa che il codice funzionale dovrebbe avere un certo "aspetto" estetico di una grande espressione annidata. Nota anche se la mia versione ricorda un po 'il codice imperativo con tutti i punti e virgola, tutto è immutabile, quindi potresti sostituire tutte le variabili e ottenere una grande espressione se lo desideri. È infatti altrettanto "funzionale" della versione spaghetti, ma con una maggiore leggibilità.

Qui le espressioni sono suddivise in pezzi molto piccoli e dati nomi significativi per il dominio. L'annidamento è evitato estraendo funzionalità comuni come mapObj in una funzione con nome. Lambdas è riservato a funzioni molto brevi con uno scopo chiaro nel contesto.

Se trovi un codice che è difficile da leggere, rifattalo finché non è più facile. Ci vuole un po 'di pratica, ma ne vale la pena. Il codice funzionale può essere altrettanto leggibile quanto imperativo. In effetti, spesso moreso, perché di solito è più conciso.

    
risposta data 11.02.2017 - 08:07
fonte
6

Non ho fatto un sacco di lavoro altamente funzionale in Javascript (che direi che questo è - la maggior parte delle persone che parlano di Javascript funzionale può usare mappe, filtri e riduce, ma il tuo codice definisce il suo valore più alto -level delle funzioni , che è un po 'più avanzato di quello), ma l'ho fatto in Haskell, e penso che almeno una parte dell'esperienza si traduca. Ti darò alcuni suggerimenti su cose che ho imparato:

La specifica dei tipi di funzioni è molto importante. Haskell non richiede di specificare quale sia il tipo di una funzione, ma includere il tipo nella definizione rende molto più semplice la lettura. Anche se Javascript non supporta la digitazione esplicita nello 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 a lavorare con definizioni di tipo come questo, rendono molto più chiaro il significato di una funzione.

La denominazione è importante, forse anche più che nella programmazione procedurale. Molti programmi funzionali sono scritti in uno stile molto teso e pesante sulla convenzione (ad esempio la convenzione che 'xs' è una lista / matrice e che 'x' è un elemento in esso è molto pervasivo) ma a meno che tu non capisca facilmente quello stile, suggerirei una denominazione più dettagliata. Guardando i nomi specifici che hai usato, "getX" è piuttosto opaco, e quindi "getXs" non aiuta molto neanche. Chiamerei "getXs" qualcosa come "applyToProperties" e "getX" sarebbe probabilmente "propertyMapper". "getPctSameXs" sarebbe quindi "percentPropertiesSameWith" ("with" è un'altra delle convenzioni - afferma che una funzione di prefiltraggio viene applicata prima di qualche altra operazione, cf le funzioni standard Haskell come zipWith )

.

Un'altra cosa importante è scrivere codice idiomatico . Ho notato che stai usando una sintassi a => b => some-expression-involving-a-and-b per produrre funzioni al curry. Questo è interessante, e potrebbe essere utile in alcune situazioni, ma qui non si sta facendo nulla che benefici delle funzioni al curry e sarebbe più idiota Javascript usare invece le tradizionali funzioni a più argomenti. Facendolo potresti rendere più facile vedere cosa sta succedendo a colpo d'occhio. Stai anche utilizzando const name = lambda-expression per definire le funzioni, dove invece sarebbe più idiota usare function name (args) { ... } . So che sono semanticamente leggermente diversi, ma se non ti affidi a queste differenze ti suggerirei di utilizzare la variante più comune quando possibile.

    
risposta data 11.02.2017 - 04:52
fonte

Leggi altre domande sui tag