Comment lire le code JavaScript fonctionnel?

9

Je crois que j'ai appris certains / beaucoup / la plupart des concepts de base sous-jacents à la programmation fonctionnelle en JavaScript. Cependant, j'ai du mal à lire spécifiquement le code fonctionnel, même le code que j'ai écrit, et je me demande si quelqu'un peut me donner des conseils, des conseils, des meilleures pratiques, de la terminologie, etc. qui peuvent vous aider.

Prenez le code ci-dessous. J'ai écrit ce code. Il vise à attribuer un pourcentage de similitude entre deux objets, entre dire {a:1, b:2, c:3, d:3}et {a:1, b:1, e:2, f:2, g:3, h:5}. J'ai produit le code en réponse à cette question sur Stack Overflow . Parce que je n'étais pas sûr du type de pourcentage de similitude que l'affiche demandait, j'ai fourni quatre types différents:

  • le pourcentage des clés du 1er objet que l'on peut trouver dans le 2ème,
  • le pourcentage des valeurs du 1er objet que l'on peut trouver dans le 2e, y compris les doublons,
  • le pourcentage des valeurs du 1er objet que l'on peut trouver dans le 2e, sans doublons autorisés, et
  • le pourcentage de paires {key: value} dans le 1er objet que l'on peut trouver dans le 2ème objet.

J'ai commencé avec un code raisonnablement impératif, mais j'ai rapidement réalisé que c'était un problème bien adapté à la programmation fonctionnelle. En particulier, je me suis rendu compte que si je pouvais extraire une fonction ou trois pour chacune des quatre stratégies ci-dessus qui définissaient le type de fonctionnalité que je cherchais à comparer (par exemple, les touches ou les valeurs, etc.), alors je pourrais être capable de réduire (pardonner le jeu de mots) le reste du code en unités répétables. Vous savez, le garder au SEC. Je suis donc passé à la programmation fonctionnelle. Je suis assez fier du résultat, je pense que c'est assez élégant et je pense que je comprends assez bien ce que j'ai fait.

Cependant, même après avoir écrit le code moi-même et en avoir compris chaque partie pendant la construction, quand j'y repense maintenant, je continue d'être un peu déconcerté à la fois comment lire une demi-ligne particulière, ainsi que comment "grok" ce que fait une demi-ligne particulière de code. Je me retrouve à faire des flèches mentales pour connecter différentes parties qui se dégradent rapidement en un gâchis de spaghettis.

Alors, quelqu'un peut-il me dire comment "lire" certains des morceaux de code les plus compliqués d'une manière à la fois concise et qui contribue à ma compréhension de ce que je lis? Je suppose que les parties qui m'obtiennent le plus sont celles qui ont plusieurs grosses flèches d'affilée et / ou des pièces qui ont plusieurs parenthèses d'affilée. Encore une fois, dans leur cœur, je peux éventuellement comprendre la logique, mais (j'espère) il existe une meilleure façon de procéder rapidement et clairement et directement "en prenant" une ligne de programmation JavaScript fonctionnelle.

N'hésitez pas à utiliser n'importe quelle ligne de code ci-dessous, ou même d'autres exemples. Cependant, si vous voulez quelques suggestions initiales de ma part, en voici quelques-unes. Commencez par une solution assez simple. Près de la fin du code, il y a ce qui est passé comme paramètre à une fonction: obj => key => obj[key]. Comment peut-on lire et comprendre cela? Un exemple plus est une fonction complète de près du début: const getXs = (obj, getX) => Object.keys(obj).map(key => getX(obj)(key));. La dernière mappartie me touche particulièrement.

S'il vous plaît noter, à ce moment je ne cherche des références à Haskell ou notation symbolique abstraite ou les fondements de corroyage, etc. Ce que je suis à la recherche est phrases en anglais que je peux en silence la bouche tout en regardant une ligne de code. Si vous avez des références qui traitent précisément de cela, tant mieux, mais je ne recherche pas non plus de réponses qui disent que je devrais aller lire certains manuels de base. Je l'ai fait et j'obtiens (au moins une grande partie de) la logique. Notez également que je n'ai pas besoin de réponses exhaustives (bien que de telles tentatives soient les bienvenues): même des réponses courtes qui fournissent une manière élégante de lire une seule ligne particulière de code autrement gênant seraient appréciées.

Je suppose qu'une partie de cette question est: puis- je même lire le code fonctionnel de façon linéaire, vous savez, de gauche à droite et de haut en bas? Ou est-on à peu près obligé de créer une image mentale d'un câblage de type spaghetti sur la page de code qui n'est décidément pas linéaire? Et si l'on doit le faire, nous devons encore lire le code, alors comment prendre du texte linéaire et câbler les spaghettis?

Des conseils seraient appréciés.

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
Andrew Willems
la source

Réponses:

18

Vous avez surtout du mal à le lire car cet exemple particulier n'est pas très lisible. Aucune infraction prévue, une proportion décourageante d'échantillons que vous trouvez sur Internet ne le sont pas non plus. Beaucoup de gens ne jouent avec la programmation fonctionnelle que le week-end et n'ont jamais vraiment à gérer le maintien du code fonctionnel de production à long terme. Je l'écrirais plus comme ceci:

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);

Pour une raison quelconque, beaucoup de gens ont cette idée dans leur tête que le code fonctionnel devrait avoir un certain "look" esthétique d'une grande expression imbriquée. Notez bien que ma version ressemble un peu au code impératif avec tous les points-virgules, tout est immuable, vous pouvez donc remplacer toutes les variables et obtenir une grande expression si vous le souhaitez. C'est en effet tout aussi "fonctionnel" que la version spaghetti, mais avec plus de lisibilité.

Ici, les expressions sont divisées en très petits morceaux et en prénoms significatifs pour le domaine. L'imbrication est évitée en tirant des fonctionnalités communes comme mapObjdans une fonction nommée. Les lambdas sont réservés à des fonctions très courtes avec un objectif clair dans le contexte.

Si vous rencontrez du code difficile à lire, refactorisez-le jusqu'à ce qu'il soit plus facile. Cela demande un peu de pratique, mais cela en vaut la peine. Le code fonctionnel peut être aussi lisible qu'impératif. En fait, souvent plus, car il est généralement plus concis.

Karl Bielefeldt
la source
Certainement aucune infraction prise! Bien que je maintienne toujours que je connais certaines choses sur la programmation fonctionnelle, mes affirmations dans la question sur ce que je sais étaient peut-être un peu exagérées. Je suis vraiment un débutant relatif. Donc, voir comment cette tentative particulière peut être réécrite d'une manière aussi concise, claire mais toujours fonctionnelle, semble être de l'or ... merci. J'étudierai attentivement votre réécriture.
Andrew Willems
1
J'ai entendu dire que le fait d'avoir de longues chaînes et / ou l'imbrication de méthodes élimine les variables intermédiaires inutiles. En revanche, votre réponse casse mes chaînes / imbrication en instructions autonomes intermédiaires à l'aide de variables intermédiaires bien nommées. Je trouve votre code plus lisible dans ce cas, mais je me demande à quel point vous essayez d'être général. Êtes-vous en train de dire que les longues chaînes de méthodes et / ou l'imbrication profonde sont souvent ou même toujours un anti-modèle à éviter, ou y a-t-il des moments où ils apportent des avantages significatifs? Et la réponse à cette question est-elle différente pour le codage fonctionnel par rapport au codage impératif?
Andrew Willems
3
Il existe certaines situations où l'élimination des variables intermédiaires peut ajouter de la clarté. Par exemple, dans FP, vous ne voulez presque jamais un index dans un tableau. Parfois aussi, il n'y a pas un grand nom pour le résultat intermédiaire. D'après mon expérience, cependant, la plupart des gens ont tendance à trop se tromper dans l'autre sens.
Karl Bielefeldt
6

Je n'ai pas fait beaucoup de travail hautement fonctionnel en Javascript (ce que je dirais que c'est - la plupart des gens qui parlent de Javascript fonctionnel peuvent utiliser des cartes, des filtres et des réductions, mais votre code définit ses propres fonctions de niveau supérieur , ce qui est un peu plus avancé que cela), mais je l'ai fait à Haskell, et je pense qu'au moins une partie de l'expérience se traduit. Je vais vous donner quelques conseils sur les choses que j'ai apprises:

La spécification des types de fonctions est vraiment importante. Haskell ne vous oblige pas à spécifier le type d'une fonction, mais l'inclusion du type dans la définition facilite la lecture. Bien que Javascript ne prenne pas en charge la saisie explicite de la même manière, il n'y a aucune raison de ne pas inclure la définition de type dans un commentaire, par exemple:

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

Avec un peu de pratique pour travailler avec des définitions de type comme celles-ci, elles rendent le sens d'une fonction beaucoup plus clair.

La dénomination est importante, peut-être plus encore que dans la programmation procédurale. Un grand nombre de programmes fonctionnels sont écrits dans un style très laconique qui est lourd de convention (par exemple, la convention que «xs» est une liste / tableau et que «x» est un élément est très répandue), mais à moins que vous ne compreniez ce style je suggérerais facilement une dénomination plus détaillée. En regardant les noms spécifiques que vous avez utilisés, "getX" est plutôt opaque, et donc "getXs" n'aide pas vraiment non plus. J'appellerais "getXs" quelque chose comme "applyToProperties", et "getX" serait probablement "propertyMapper". "getPctSameXs" serait alors "percentPropertiesSameWith" ("avec").

Une autre chose importante est d' écrire du code idiomatique . Je remarque que vous utilisez une syntaxe a => b => some-expression-involving-a-and-bpour produire des fonctions au curry. Ceci est intéressant et pourrait être utile dans certaines situations, mais vous ne faites rien ici qui bénéficie des fonctions curry et il serait plus idiomatique Javascript d'utiliser à la place des fonctions traditionnelles à plusieurs arguments. Cela peut faciliter la vue d'ensemble de ce qui se passe. Vous utilisez également const name = lambda-expressionpour définir des fonctions, où il serait plus idiomatique d'utiliser à la function name (args) { ... }place. Je sais qu'ils sont sémantiquement légèrement différents, mais à moins que vous ne vous appuyiez sur ces différences, je suggère d'utiliser la variante la plus courante lorsque cela est possible.

Jules
la source
5
+1 pour les types! Ce n'est pas parce que la langue n'en a pas que vous n'avez pas à y penser . Plusieurs systèmes de documentation pour ECMAScript ont un langage de type pour enregistrer les types de fonctions. Plusieurs IDE ECMAScript ont également un langage de type (et généralement, ils comprennent également les langages de type pour les principaux systèmes de documentation), et ils peuvent même effectuer une vérification de type rudimentaire et des indices heuristiques à l'aide de ces annotations de type .
Jörg W Mittag
Vous m'avez beaucoup donné à mâcher: définitions de types, noms significatifs, utilisation d'expressions idiomatiques ... merci! Juste quelques-uns des nombreux commentaires possibles: je n'avais pas nécessairement l'intention d'écrire certaines parties en tant que fonctions curry; ils ont juste évolué de cette façon alors que je refactorisais mon code lors de l'écriture. Je peux maintenant voir comment cela n'était pas nécessaire, et même simplement fusionner les paramètres de ces deux fonctions en deux paramètres pour une seule fonction est non seulement plus logique, mais rend instantanément ce petit morceau au moins plus lisible.
Andrew Willems
@ JörgWMittag, merci pour vos commentaires sur l'importance des types et pour le lien vers cette autre réponse que vous avez écrite. J'utilise WebStorm et je ne savais pas que, selon la façon dont je lis votre autre réponse, WebStorm sait comment interpréter les annotations de type jsdoc. Je suppose de votre commentaire que jsdoc et WebStorm peuvent être utilisés ensemble pour annoter du code fonctionnel, pas seulement impératif, mais je devrais approfondir pour vraiment le savoir. J'ai déjà joué avec jsdoc et maintenant que je sais que WebStorm et moi pouvons y coopérer, je m'attends à ce que j'utilise davantage cette fonctionnalité / approche.
Andrew Willems
@Jules, juste pour clarifier à quelle fonction curry je faisais référence dans mon commentaire ci-dessus: Comme vous l'avez laissé entendre, chaque instance de obj => key => ...peut être simplifiée (obj, key) => ...car plus tard getX(obj)(key)peut également être simplifiée get(obj, key). En revanche, une autre fonction curry,, (getX, filter = vals => vals) => (objA, objB) => ...ne peut pas être facilement simplifiée, au moins dans le contexte du reste du code tel qu'il est écrit.
Andrew Willems