Est-ce une pure fonction?

117

La plupart des sources définissent une fonction pure comme ayant les deux propriétés suivantes:

  1. Sa valeur de retour est la même pour les mêmes arguments.
  2. Son évaluation n'a pas d'effets secondaires.

C'est la première condition qui me concerne. Dans la plupart des cas, il est facile de juger. Tenez compte des fonctions JavaScript suivantes (comme indiqué dans cet article )

Pur:

const add = (x, y) => x + y;

add(2, 4); // 6

Impur:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

Il est facile de voir que la 2ème fonction donnera différentes sorties pour les appels suivants, violant ainsi la première condition. Et donc, c'est impur.

Je reçois cette partie.


Maintenant, pour ma question, considérons cette fonction qui convertit un montant donné en dollars en euros:

(EDIT - Utilisation constdans la première ligne. Utilisé letplus tôt par inadvertance.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Supposons que nous récupérons le taux de change à partir d'une base de données et qu'il change chaque jour.

Maintenant, peu importe combien de fois j'appelle cette fonction aujourd'hui , cela me donnera la même sortie pour l'entrée 100. Cependant, cela pourrait me donner une sortie différente demain. Je ne sais pas si cela viole la première condition ou non.

IOW, la fonction elle-même ne contient aucune logique pour muter l'entrée, mais elle s'appuie sur une constante externe qui pourrait changer à l'avenir. Dans ce cas, il est absolument certain que cela changera quotidiennement. Dans d'autres cas, cela peut arriver; il se pourrait que non.

Pouvons-nous appeler de telles fonctions des fonctions pures. Si la réponse est NON, comment pouvons-nous la refactoriser pour en faire une?

Bonhomme de neige
la source
6
La pureté d'un langage aussi dynamique que JS est un sujet très compliqué:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms
29
La pureté signifie que vous pouvez remplacer l'appel de fonction par sa valeur de résultat au niveau du code sans changer le comportement de votre programme.
bob
1
Pour aller un peu plus loin sur ce qui constitue un effet secondaire, et avec une terminologie plus théorique, voir cs.stackexchange.com/questions/116377/…
Gilles 'SO- arrête d'être mauvais'
3
Aujourd'hui, la fonction est (x) => {return x * 0.9;}. Demain, vous aurez une fonction différente qui sera aussi pure, peut-être (x) => {return x * 0.89;}. Notez que chaque fois que vous l'exécutez, (x) => {return x * exchangeRate;}il crée une nouvelle fonction, et cette fonction est pure car exchangeRatene peut pas changer.
user253751
2
Ceci est une fonction impure, si vous voulez la rendre pure, vous pouvez l'utiliser const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; pour une fonction pure, Its return value is the same for the same arguments.devrait toujours tenir, 1 seconde, 1 décennie .. plus tard, peu importe quoi
Vikash Tiwari

Réponses:

133

La dollarToEurovaleur de retour de 'dépend d'une variable externe qui n'est pas un argument; par conséquent, la fonction est impure.

Dans la réponse est NON, comment alors refactoriser la fonction pour qu'elle soit pure?

Une option consiste à passer exchangeRate. De cette façon, tous les arguments de temps sont (something, somethingElse), la sortie est garantie d'être something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Notez que pour la programmation fonctionnelle, vous devez éviter let- utilisez toujours constpour éviter la réaffectation.

CertainPerformance
la source
6
Ne pas avoir de variables libres n'est pas une condition pour qu'une fonction soit pure: const add = x => y => x + y; const one = add(42);ici les deux addet onesont des fonctions pures.
zerkms
7
const foo = 42; const add42 = x => x + foo;<- ceci est une autre fonction pure, qui utilise à nouveau des variables libres.
zerkms
8
@zerkms - Je serais très heureux de voir votre réponse à cette question (même si elle reformule simplement CertainPerformance pour utiliser une terminologie différente). Je ne pense pas que ce serait en double, et ce serait éclairant, en particulier lorsqu'il est cité (idéalement avec de meilleures sources que l'article Wikipedia ci-dessus, mais si c'est tout ce que nous obtenons, toujours une victoire). (Il serait facile de lire ce commentaire dans une sorte de lumière négative. Croyez-moi que je suis sincère, je pense qu'une telle réponse serait formidable et j'aimerais la lire.)
TJ Crowder
17
Je pense que vous et @zerkms avez tort. Vous semblez penser que la dollarToEurofonction dans l'exemple de votre réponse est impure car elle dépend de la variable libre exchangeRate. C'est absurde. Comme l'a souligné zerkms, la pureté d'une fonction n'a rien à voir avec le fait qu'elle ait ou non des variables libres. Cependant, zerkms a également tort car il pense que la dollarToEurofonction est impure car elle dépend de celle exchangeRatequi provient d'une base de données. Il dit que c'est impur parce que "cela dépend de manière transitoire de l'OI".
Aadit M Shah
9
(suite) Encore une fois, c'est absurde car cela suggère que dollarToEuroc'est impur parce que exchangeRatec'est une variable libre. Cela suggère que si ce exchangeRaten'était pas une variable libre, c'est-à-dire si c'était un argument, alors ce dollarToEuroserait pur. Par conséquent, cela suggère que dollarToEuro(100)c'est impur mais dollarToEuro(100, exchangeRate)pur. C'est clairement absurde, car dans les deux cas, vous dépendez de ce exchangeRatequi provient d'une base de données. La seule différence est la présence ou non d' exchangeRateune variable libre dans la dollarToEurofonction.
Aadit M Shah
76

Techniquement, tout programme que vous exécutez sur un ordinateur est impur car il se compile finalement en instructions telles que "déplacer cette valeur dans eax" et "ajouter cette valeur au contenu de eax", qui sont impures. Ce n'est pas très utile.

Au lieu de cela, nous pensons à la pureté en utilisant des boîtes noires . Si un code produit toujours les mêmes sorties lorsqu'il reçoit les mêmes entrées, il est considéré comme pur. Selon cette définition, la fonction suivante est également pure même si en interne elle utilise une table de mémo impure.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

Nous ne nous soucions pas des composants internes, car nous utilisons une méthodologie de boîte noire pour vérifier la pureté. De même, nous ne nous soucions pas que tout le code soit finalement converti en instructions machine impures parce que nous pensons à la pureté en utilisant une méthodologie de boîte noire. Les internes ne sont pas importants.

Maintenant, considérez la fonction suivante.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

La greetfonction est-elle pure ou impure? Selon notre méthodologie de boîte noire, si nous lui donnons la même entrée (par exemple World), il imprime toujours la même sortie à l'écran (par exemple Hello World!). En ce sens, n'est-ce pas pur? Non ce n'est pas. La raison pour laquelle ce n'est pas pur est parce que nous considérons l'impression de quelque chose à l'écran comme un effet secondaire. Si notre boîte noire produit des effets secondaires, elle n'est pas pure.

Qu'est-ce qu'un effet secondaire? C’est là que le concept de transparence référentielle est utile. Si une fonction est référentiellement transparente, nous pouvons toujours remplacer les applications de cette fonction par leurs résultats. Notez que ce n'est pas la même chose que la fonction inline .

Dans la fonction inline, nous remplaçons les applications d'une fonction par le corps de la fonction sans altérer la sémantique du programme. Cependant, une fonction référentiellement transparente peut toujours être remplacée par sa valeur de retour sans altérer la sémantique du programme. Prenons l'exemple suivant.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Ici, nous avons souligné la définition de greetet cela n'a pas changé la sémantique du programme.

Maintenant, considérez le programme suivant.

undefined;
undefined;

Ici, nous avons remplacé les applications du greet fonction par leurs valeurs de retour et cela a changé la sémantique du programme. Nous n'imprimons plus de salutations à l'écran. C'est la raison pour laquelle l'impression est considérée comme un effet secondaire, et c'est pourquoi la greetfonction est impure. Ce n'est pas référentiellement transparent.

Maintenant, considérons un autre exemple. Considérez le programme suivant.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

De toute évidence, la mainfonction est impure. Cependant, la timeDifffonction est-elle pure ou impure? Bien que cela dépendeserverTime ce qui provient d'un appel réseau impur, il est toujours référentiellement transparent car il renvoie les mêmes sorties pour les mêmes entrées et parce qu'il n'a aucun effet secondaire.

zerkms sera probablement en désaccord avec moi sur ce point. Dans sa réponse , il a dit que la dollarToEurofonction dans l'exemple suivant est impure parce que "cela dépend transitoirement de l'IO".

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Je dois être en désaccord avec lui parce que le fait que le exchangeRate provienne d'une base de données n'est pas pertinent. C'est un détail interne et notre méthodologie de boîte noire pour déterminer la pureté d'une fonction ne se soucie pas des détails internes.

Dans les langages purement fonctionnels comme Haskell, nous avons une trappe d'échappement pour exécuter des effets d'E / S arbitraires. C'est appeléunsafePerformIO , et comme son nom l'indique, si vous ne l'utilisez pas correctement, ce n'est pas sûr, car cela pourrait briser la transparence référentielle. Cependant, si vous savez ce que vous faites, son utilisation est parfaitement sûre.

Il est généralement utilisé pour charger des données à partir de fichiers de configuration au début du programme. Le chargement de données à partir de fichiers de configuration est une opération d'E / S impure. Cependant, nous ne voulons pas être gênés par le passage des données en entrée à chaque fonction. Par conséquent, si nous utilisons unsafePerformIOalors nous pouvons charger les données au niveau supérieur et toutes nos fonctions pures peuvent dépendre des données de configuration globale immuables.

Notez que ce n'est pas parce qu'une fonction dépend de certaines données chargées à partir d'un fichier de configuration, d'une base de données ou d'un appel réseau que la fonction est impure.

Cependant, considérons votre exemple original qui a une sémantique différente.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Ici, je suppose que parce que exchangeRaten'est pas défini comme const, il va être modifié pendant l'exécution du programme. Si tel est le cas, alors dollarToEuroc'est définitivement une fonction impure parce que lorsque le exchangeRateest modifié, il brisera la transparence référentielle.

Cependant, si la exchangeRatevariable n'est pas modifiée et ne sera jamais modifiée à l'avenir (c'est-à-dire si c'est une valeur constante), alors même si elle est définie comme let, elle ne cassera pas la transparence référentielle. Dans ce cas, dollarToEuroest en effet une fonction pure.

Notez que la valeur de exchangeRatepeut changer à chaque fois que vous exécutez à nouveau le programme et qu'elle ne cassera pas la transparence référentielle. Il ne rompt la transparence référentielle que s'il change pendant l'exécution du programme.

Par exemple, si vous exécutez mon timeDiffexemple plusieurs fois, vous obtiendrez des valeurs différentes serverTimeet donc des résultats différents. Toutefois, comme la valeur de serverTimene change jamais pendant l'exécution du programme, la timeDifffonction est pure.

Aadit M Shah
la source
3
C'était très instructif. Merci. Et je voulais utiliser constdans mon exemple.
Snowman
3
Si vous aviez l'intention de l'utiliser, constla dollarToEurofonction est en effet pure. La seule façon dont la valeur de exchangeRatechange serait si vous réexécutiez le programme. Dans ce cas, l'ancien processus et le nouveau processus sont différents. Par conséquent, il ne rompt pas la transparence référentielle. C'est comme appeler deux fois une fonction avec des arguments différents. Les arguments peuvent être différents mais dans la fonction, la valeur des arguments reste constante.
Aadit M Shah
3
Cela ressemble à une petite théorie de la relativité: les constantes ne sont que relativement constantes, pas absolument, à savoir par rapport au processus en cours. De toute évidence, la seule bonne réponse ici. +1.
bob
5
Je ne suis pas d'accord avec "est impur car il compile finalement des instructions comme" déplacer cette valeur dans eax "et" ajouter cette valeur au contenu de eax " . S'il eaxest effacé - via une charge ou un effacement - le code reste déterministe indépendamment de ce qui se passe d'autre et est donc pur. Sinon, réponse très complète
3Dave
3
@Bergi: En fait, dans un langage pur avec des valeurs immuables, l'identité n'a pas d'importance. Le fait de savoir si deux références évaluées à la même valeur sont deux références au même objet ou à des objets différents ne peut être observé qu'en mutant l'objet via l'une des références et en observant si la valeur change également lorsqu'elle est récupérée via l'autre référence. Sans mutation, l'identité devient hors de propos. (Comme dirait Rich Hickey: l'identité est une série d'États dans le temps.)
Jörg W Mittag
23

Une réponse d'un moi-puriste (où "moi" est littéralement moi, car je pense que cette question n'a pas un seul formel "bonne" réponse ):

Dans un langage dynamique tel que JS avec autant de possibilités pour modifier les types de base de patchs, ou créer des types personnalisés en utilisant des fonctionnalités telles que Object.prototype.valueOf il est impossible de dire si une fonction est pure juste en la regardant, car c'est à l'appelant de décider s'il veut pour produire des effets secondaires.

Une démo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Une réponse de moi-pragmatique:

De la définition même de wikipedia

En programmation informatique, une fonction pure est une fonction qui a les propriétés suivantes:

  1. Sa valeur de retour est la même pour les mêmes arguments (pas de variation avec les variables statiques locales, les variables non locales, les arguments de référence mutables ou les flux d'entrée des périphériques d'E / S).
  2. Son évaluation n'a pas d'effets secondaires (pas de mutation des variables statiques locales, des variables non locales, des arguments de référence mutables ou des flux d'E / S).

En d'autres termes, il importe uniquement comment une fonction se comporte, pas comment elle est implémentée. Et tant qu'une fonction particulière détient ces 2 propriétés - elle est pure quelle que soit la façon dont elle a été implémentée.

Passons maintenant à votre fonction:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Il est impur car il ne qualifie pas l'exigence 2: il dépend de l'IO de manière transitoire.

J'accepte que la déclaration ci-dessus soit erronée, voir l'autre réponse pour plus de détails: https://stackoverflow.com/a/58749249/251311

Autres ressources pertinentes:

zerkms
la source
4
@TJCrowder en metant que zerkms qui fournit une réponse.
zerkms
2
Ouais, avec Javascript, c'est une question de confiance, pas de garantie
bob
4
@bob ... ou c'est un appel bloquant.
zerkms
1
@zerkms - Merci. Juste pour être sûr à 100%, la principale différence entre le vôtre add42et le mien addXest purement que mon xpeut être changé, et votre ftne peut pas être changé (et donc, add42la valeur de retour de ne varie pas en fonction de ft)?
TJ Crowder du
5
Je ne suis pas d'accord que la dollarToEurofonction dans votre exemple est impure. J'ai expliqué pourquoi je n'étais pas d'accord dans ma réponse. stackoverflow.com/a/58749249/783743
Aadit M Shah
14

Comme d'autres réponses l'ont dit, la façon dont vous avez mis en œuvre dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

est en effet pur, car le taux de change n'est pas mis à jour pendant l'exécution du programme. Conceptuellement, cependant, il dollarToEurosemble que ce devrait être une fonction impure, dans la mesure où il utilise le taux de change le plus récent. La façon la plus simple d'expliquer cet écart est que vous n'avez pas implémenté dollarToEuromaisdollarToEuroAtInstantOfProgramStart .

La clé ici est qu'il existe plusieurs paramètres qui sont nécessaires pour calculer une conversion monétaire, et qu'une version vraiment pure du général dollarToEuroles fournirait tous. Les paramètres les plus directs sont le montant en USD à convertir et le taux de change. Cependant, comme vous souhaitez obtenir votre taux de change à partir des informations publiées, vous avez maintenant trois paramètres à fournir:

  • Le montant d'argent à échanger
  • Une autorité historique pour consulter les taux de change
  • La date à laquelle la transaction a eu lieu (pour indexer l'autorité historique)

L'autorité historique ici est votre base de données, et en supposant que la base de données n'est pas compromise, retournera toujours le même résultat pour le taux de change un jour particulier. Par conséquent, avec la combinaison de ces trois paramètres, vous pouvez écrire une version entièrement pure et autosuffisante du général dollarToEuro, qui pourrait ressembler à ceci:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Votre implémentation capture des valeurs constantes pour l'autorité historique et la date de la transaction au moment où la fonction est créée - l'autorité historique est votre base de données et la date capturée est la date à laquelle vous démarrez le programme - tout ce qui reste est le montant en dollars , que l'appelant fournit. La version impure dedollarToEuro qui obtient toujours la valeur la plus à jour prend essentiellement le paramètre date implicitement, le définissant à l'instant où la fonction est appelée, ce qui n'est pas pur simplement parce que vous ne pouvez jamais appeler la fonction avec les mêmes paramètres deux fois.

Si vous voulez en avoir une version pure dollarToEuroqui peut toujours obtenir la valeur la plus à jour, vous pouvez toujours lier l'autorité historique, mais laissez le paramètre date non lié et demandez la date à l'appelant comme argument, en finissant par avec quelque chose comme ça:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());
TheHansinator
la source
@Snowman Vous êtes les bienvenus! J'ai mis à jour un peu la réponse pour ajouter d'autres exemples de code.
TheHansinator
8

Je voudrais revenir un peu sur les détails spécifiques de JS et l'abstraction des définitions formelles, et parler des conditions à respecter pour permettre des optimisations spécifiques. C'est généralement la principale chose dont nous nous soucions lors de l'écriture de code (bien que cela aide également à prouver l'exactitude). La programmation fonctionnelle n'est ni un guide des dernières tendances ni un vœu monastique d'abnégation. C'est un outil pour résoudre des problèmes.

Lorsque vous avez un code comme celui-ci:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

S'il exchangeRaten'a jamais pu être modifié entre les deux appels vers dollarToEuro(100), il est possible de mémoriser le résultat du premier appel dollarToEuro(100)et d'optimiser le deuxième appel. Le résultat sera le même, donc nous pouvons simplement nous souvenir de la valeur d'avant.

Le exchangeRatepeut être défini une fois, avant d'appeler une fonction qui le recherche, et jamais modifié. De manière moins restrictive, vous pouvez avoir un code qui recherche une exchangeRatefois une fonction ou un bloc de code particulier et utilise le même taux de change de manière cohérente dans cette étendue. Ou, si seulement ce fil peut modifier la base de données, vous pourriez supposer que si vous n'avez pas mis à jour le taux de change, personne d'autre ne l'a modifié sur vous.

Si fetchFromDatabase()elle-même est une fonction pure évaluant une constante et exchangeRateest immuable, nous pourrions plier cette constante tout au long du calcul. Un compilateur qui sait que c'est le cas pourrait faire la même déduction que vous avez fait dans le commentaire, qui est dollarToEuro(100)évalué à 90,0, et remplacer l'expression entière par la constante 90,0.

Cependant, s'il fetchFromDatabase()n'effectue pas d'E / S, ce qui est considéré comme un effet secondaire, son nom viole le principe du moindre étonnement.

Davislor
la source
8

Cette fonction n'est pas pure, elle s'appuie sur une variable extérieure, qui va presque certainement changer.

La fonction échoue donc au premier point que vous avez fait, elle ne retourne pas la même valeur quand pour les mêmes arguments.

Pour rendre cette fonction "pure", passez exchangeRateen argument.

Cela satisferait alors les deux conditions.

  1. Il retournerait toujours la même valeur en passant la même valeur et le même taux de change.
  2. Cela n'aurait également aucun effet secondaire.

Exemple de code:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())
Jessica
la source
1
"qui va presque certainement changer" --- ce n'est pas le cas const.
zerkms
7

Pour développer les points soulevés par d'autres sur la transparence référentielle: nous pouvons définir la pureté comme étant simplement la transparence référentielle des appels de fonction (c'est-à-dire que chaque appel à la fonction peut être remplacé par la valeur de retour sans changer la sémantique du programme).

Les deux propriétés que vous donnez sont toutes deux des conséquences de la transparence référentielle. Par exemple, la fonction suivante f1est impure, car elle ne donne pas le même résultat à chaque fois (la propriété que vous avez numérotée 1):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Pourquoi est-il important d'obtenir le même résultat à chaque fois? Parce que l'obtention de résultats différents est un moyen pour un appel de fonction d'avoir une sémantique différente d'une valeur, et donc de rompre la transparence référentielle.

Disons que nous écrivons le code f1("hello", "world"), nous l'exécutons et obtenons la valeur de retour "hello". Si nous effectuons une recherche / remplacement de chaque appel f1("hello", "world")et les remplaçons par, "hello"nous aurons changé la sémantique du programme (tous les appels seront désormais remplacés par "hello", mais à l'origine environ la moitié d'entre eux auraient été évalués "world"). Les appels à f1ne sont donc pas référentiellement transparents, donc f1impurs.

Une autre façon dont un appel de fonction peut avoir une sémantique différente d'une valeur consiste à exécuter des instructions. Par exemple:

function f2(x) {
  console.log("foo");
  return x;
}

La valeur de retour de f2("bar")sera toujours "bar", mais la sémantique de la valeur "bar"est différente de l'appel f2("bar")puisque ce dernier se connectera également à la console. Remplacer l'un par l'autre changerait la sémantique du programme, donc il n'est pas référentiellement transparent, et donc f2impur.

Que votre dollarToEurofonction soit référentiellement transparente (et donc pure) dépend de deux choses:

  • La «portée» de ce que nous considérons comme référentiellement transparent
  • Si la exchangeRatevolonté changera jamais dans cette «portée»

Il n'y a pas de «meilleur» champ d'application à utiliser; normalement, nous pensons à une seule exécution du programme ou à la durée de vie du projet. Par analogie, imaginez que les valeurs de retour de chaque fonction soient mises en cache (comme la table de mémo dans l'exemple donné par @ aadit-m-shah): quand aurions-nous besoin de vider le cache, pour garantir que les valeurs périmées n'interfèrent pas avec notre sémantique?

Si vous l' exchangeRateutilisiez, varcela pourrait changer entre chaque appel à dollarToEuro; nous aurions besoin d'effacer les résultats mis en cache entre chaque appel, il n'y aurait donc pas de transparence référentielle à proprement parler.

En utilisant, constnous étendons la «portée» à une exécution du programme: il serait sûr de mettre en cache les valeurs de retour dollarToEurojusqu'à la fin du programme. Nous pourrions imaginer utiliser une macro (dans un langage comme Lisp) pour remplacer les appels de fonction par leurs valeurs de retour. Cette quantité de pureté est courante pour des éléments tels que les valeurs de configuration, les options de ligne de commande ou les ID uniques. Si nous nous limitons à penser à une exécution du programme, nous obtenons la plupart des avantages de la pureté, mais nous devons être prudents entre les exécutions (par exemple, enregistrer des données dans un fichier, puis les charger dans une autre exécution). Je n'appellerais pas de telles fonctions "pures" dans un sens abstrait (par exemple si j'écrivais une définition de dictionnaire), mais je n'ai aucun problème à les traiter comme pures dans leur contexte .

Si nous considérons la durée de vie du projet comme notre «portée», nous sommes alors «le plus référentiellement transparent» et donc le «plus pur», même dans un sens abstrait. Nous n'aurions jamais besoin de vider notre cache hypothétique. Nous pourrions même faire cette "mise en cache" en réécrivant directement le code source sur le disque, pour remplacer les appels par leurs valeurs de retour. Cela pourrait même fonctionner sur plusieurs projets, par exemple, nous pourrions imaginer une base de données en ligne de fonctions et leurs valeurs de retour, où n'importe qui peut rechercher un appel de fonction et (s'il est dans la base de données) utiliser la valeur de retour fournie par quelqu'un de l'autre côté de la monde qui a utilisé une fonction identique il y a des années sur un projet différent.

Warbo
la source
4

Comme écrit, c'est une fonction pure. Il ne produit aucun effet secondaire. La fonction a un paramètre formel, mais elle a deux entrées et produira toujours la même valeur pour deux entrées quelconques.

11112222233333
la source
2

Pouvons-nous appeler de telles fonctions des fonctions pures. Si la réponse est NON, comment pouvons-nous la refactoriser pour en faire une?

Comme vous l'avez bien noté, "cela pourrait me donner un résultat différent demain" . Si tel était le cas, la réponse serait un "non" retentissant . Cela est particulièrement vrai si votre comportement souhaité dollarToEuroa été correctement interprété comme:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Cependant, une interprétation différente existe, où elle serait considérée comme pure:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro directement au-dessus est pur.


Du point de vue de l'ingénierie logicielle, il est essentiel de déclarer la dépendance de dollarToEurola fonction fetchFromDatabase. Par conséquent, remaniez la définition de ce dollarToEuroqui suit:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Avec ce résultat, étant donné la prémisse qui fetchFromDatabasefonctionne de manière satisfaisante, alors nous pouvons conclure que la projection de fetchFromDatabaseon dollarToEurodoit être satisfaisante. Ou la déclaration " fetchFromDatabaseest pur" implique dollarToEuroest pur (car fetchFromDatabaseest une base pour dollarToEuropar le facteur scalaire de x.

D'après le message d'origine, je peux comprendre que fetchFromDatabasec'est un temps de fonction. Améliorons l'effort de refactorisation pour rendre cette compréhension transparente, et donc clairement qualifiée fetchFromDatabasede fonction pure:

fetchFromDatabase = (horodatage) => {/ * voici l'implémentation * /};

En fin de compte, je voudrais refactoriser la fonctionnalité comme suit:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Par conséquent, dollarToEuropeut être testé unitaire en prouvant simplement qu'il appelle correctement fetchFromDatabase(ou sa dérivée exchangeRate).

Igwe Kalu
la source
1
C'était très éclairant. +1. Merci.
Bonhomme de neige
Bien que je trouve votre réponse plus informative, et peut-être la meilleure refactorisation pour le cas d'utilisation particulier de dollarToEuro; Je l'ai mentionné dans le PO qu'il pourrait y avoir d'autres cas d'utilisation. J'ai choisi dollarToEuro parce qu'il évoque instantanément ce que j'essaie de faire, mais il pourrait y avoir quelque chose de moins subtil qui dépend d'une variable libre qui peut changer, mais pas nécessairement en fonction du temps. Dans cet esprit, je trouve que le refactorisé le plus voté est le plus accessible et celui qui peut aider les autres avec des cas d'utilisation similaires. Merci pour votre aide malgré tout.
Bonhomme de neige
-1

Je suis bilingue Haskell / JS et Haskell est l'une des langues qui fait beaucoup pour la pureté des fonctions, j'ai donc pensé vous donner la perspective de la façon dont Haskell le voit.

Comme d'autres l'ont dit, dans Haskell, la lecture d'une variable mutable est généralement considérée comme impure. Il y a une différence entre les variables et les définitions dans la mesure où les variables peuvent changer plus tard, les définitions sont les mêmes pour toujours. Donc, si vous l' aviez déclaré à ce moment- constlà (en supposant qu'il ne s'agit que d'un numberet qu'il n'a pas de structure interne modifiable), lire à partir de cela serait utiliser une définition, qui est pure. Mais vous vouliez modéliser les taux de change évoluant avec le temps, et cela nécessite une sorte de mutabilité, puis vous entrez dans l'impureté.

Pour décrire ce genre de choses impures (nous pouvons les appeler «effets» et leur utilisation «efficace» par opposition à «pur») dans Haskell, nous faisons ce que vous pourriez appeler la métaprogrammation . Aujourd'hui, la métaprogrammation fait généralement référence à des macros, ce qui n'est pas ce que je veux dire, mais plutôt l'idée d'écrire un programme pour écrire un autre programme en général.

Dans ce cas, en Haskell, nous écrivons un calcul pur qui calcule un programme efficace qui fera alors ce que nous voulons. Donc, tout l'intérêt d'un fichier source Haskell (au moins, celui qui décrit un programme, pas une bibliothèque) est de décrire un calcul pur pour un programme efficace qui produit-void, appelé main. Ensuite, le travail du compilateur Haskell consiste à prendre ce fichier source, à effectuer ce calcul pur et à placer ce programme efficace en tant qu'exécutable binaire quelque part sur votre disque dur pour l'exécuter plus tard à votre guise. Il y a un écart, en d'autres termes, entre le moment où le calcul pur s'exécute (tandis que le compilateur rend l'exécutable) et le moment où le programme efficace s'exécute (chaque fois que vous exécutez l'exécutable).

Donc pour nous, les programmes efficaces sont vraiment une structure de données et ils ne font rien intrinsèquement juste en étant mentionnés (ils n'ont pas d'effets * secondaires- * en plus de leur valeur de retour; leur valeur de retour contient leurs effets). Pour un exemple très léger d'une classe TypeScript qui décrit des programmes immuables et certaines choses que vous pouvez faire avec eux,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

La clé est que si vous en avez un, Program<x>aucun effet secondaire ne s'est produit et ce sont des entités totalement fonctionnellement pures. Le mappage d'une fonction sur un programme n'a aucun effet secondaire à moins que la fonction ne soit pas une fonction pure; séquencer deux programmes n'a pas d'effets secondaires; etc.

Ainsi, par exemple, comment appliquer cela dans votre cas, vous pouvez écrire des fonctions pures qui renvoient des programmes pour obtenir des utilisateurs par ID et pour modifier une base de données et récupérer des données JSON, comme

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

puis vous pouvez décrire un travail cron pour boucler une URL et rechercher un employé et informer son superviseur d'une manière purement fonctionnelle comme

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Le fait est que chaque fonction ici est une fonction complètement pure; rien ne s'est réellement passé jusqu'à ce que je le action.run()mette en marche. De plus, je peux écrire des fonctions comme,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

et si JS avait promis l'annulation, nous pourrions avoir deux programmes en course et prendre le premier résultat et annuler le second. (Je veux dire que nous pouvons toujours, mais il devient moins clair que faire.)

De même, dans votre cas, nous pouvons décrire l'évolution des taux de change avec

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

et exchangeRatepourrait être un programme qui examine une valeur mutable,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

mais même ainsi, cette fonction dollarsToEuros est maintenant une fonction pure d'un nombre à un programme qui produit un nombre, et vous pouvez raisonner à ce sujet de cette manière équationnelle déterministe que vous pouvez raisonner à propos de tout programme qui n'a pas d'effets secondaires.

Le coût, bien sûr, c'est que vous devrez éventuellement appeler cela .run() quelque part , et ce sera impur. Mais toute la structure de votre calcul peut être décrite par un calcul pur, et vous pouvez pousser l'impureté aux marges de votre code.

CR Drost
la source
Je suis curieux de savoir pourquoi cela continue de faire l'objet d'un vote négatif, mais je veux dire que je le maintiens toujours (c'est en fait la façon dont vous manipulez les programmes dans Haskell où les choses sont pures par défaut) et je serai ravi de réduire les votes négatifs. Pourtant, si les downvoters voulaient laisser des commentaires expliquant ce qu'ils n'aiment pas, je peux essayer de l'améliorer.
CR Drost
Ouais, je me demandais pourquoi il y avait autant de downvotes mais pas un seul commentaire, à côté bien sûr de l'auteur.
Buda Örs