web-dev-qa-db-fra.com

Est-ce une pure fonction?

La plupart 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 préoccupe. Dans la plupart des cas, il est facile de juger. Considérez les 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 des sorties différentes 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 de const dans la première ligne. Utilisé let plus 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?

117
Snowman

La valeur de retour de dollarToEuro 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, chaque fois que les arguments sont (something, somethingElse), la sortie est garantie pour ê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 const pour éviter une réaffectation.

133

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

Au lieu de cela, nous pensons à la pureté en utilisant cases 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, peu nous importe que tout le code soit finalement converti en instructions machine impures, car 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 fonction greet 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 (c'est-à-dire 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 fonction inlining .

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 greet et cela n'a pas changé la sémantique du programme.

Maintenant, considérez le programme suivant.

undefined;
undefined;

Ici, nous avons remplacé les applications de la fonction greet 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 fonction greet 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 fonction main est impure. Cependant, la fonction timeDiff est-elle pure ou impure? Bien qu'il dépende de serverTime 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 son réponse , il a dit que la fonction dollarToEuro dans l'exemple suivant est impure parce que "cela dépend de la IO transitivement."

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 car le fait que le exchangeRate provienne d'une base de données est sans importance. 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 arbitraires IO. Cela s'appelle 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, il est parfaitement sûr à utiliser.

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 impure IO. Cependant, nous ne voulons pas être surchargés en passant les données en tant qu'entrées à chaque fonction. Par conséquent, si nous utilisons unsafePerformIO alors nous pouvons charger les données au niveau supérieur et toutes nos fonctions pures peuvent dépendre des données de configuration globales 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 exchangeRate n'est pas défini comme const, il va être modifié pendant l'exécution du programme. Si c'est le cas, alors dollarToEuro est définitivement une fonction impure, car lorsque exchangeRate est modifié, cela brise la transparence référentielle.

Cependant, si la variable exchangeRate 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 le référentiel transparence. Dans ce cas, dollarToEuro est en effet une fonction pure.

Notez que la valeur de exchangeRate peut 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 exemple timeDiff plusieurs fois, vous obtiendrez des valeurs différentes pour serverTime et donc des résultats différents. Cependant, étant donné que la valeur de serverTime ne change jamais pendant l'exécution du programme, la fonction timeDiff est pure.

74
Aadit M Shah

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

Dans un langage dynamique tel que JS avec autant de possibilités pour modifier les types de base de correctifs, ou créer des types personnalisés en utilisant des fonctionnalités comme Object.prototype.valueOf il est impossible de dire si une fonction est pure simplement en la regardant, car c'est à l'appelant de décider s'il veut 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 très définition 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 seulement 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 du IO transitoirement.

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

Autres ressources pertinentes:

22
zerkms

Comme d'autres réponses l'ont dit, la façon dont vous avez implémenté 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, dollarToEuro semble ê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é dollarToEuro mais dollarToEuroAtInstantOfProgramStart.

La clé ici est qu'il y a plusieurs paramètres qui sont nécessaires pour calculer une conversion monétaire, et qu'une version vraiment pure du général dollarToEuro les fournirait tous. Les paramètres les plus directs sont le montant de l'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 dollarToEuro général, 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 de dollarToEuro 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 avoir une version pure de dollarToEuro qui 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, aboutissant à quelque chose comme ceci:

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

dollarToEuro(100, Date.now());
14
TheHansinator

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 exchangeRate comme 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())
8
Jessica

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

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

exchangeRate peut être défini une fois, avant d'appeler une fonction qui le recherche, et jamais modifié. De façon moins restrictive, vous pouvez avoir du code qui recherche une fois le exchangeRate pour 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() est lui-même une fonction pure évaluant une constante, et exchangeRate est 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, que dollarToEuro(100) évalue à 90,0 et remplacer l'expression entière par la constante 90,0.

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

8
Davislor

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 à la fois conséquences de transparence référentielle. Par exemple, la fonction suivante f1 Est 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 faisons 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 maintenant remplacés par "hello", mais à l'origine, environ la moitié d'entre eux auraient été évalués à "world"). Par conséquent, les appels à f1 Ne sont pas référentiellement transparents, donc f1 Est impur.

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 sera également connectez-vous à 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 f2 Est impur.

Que votre fonction dollarToEuro 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 le exchangeRate changera jamais dans cette "portée"

Il n'y a pas de "meilleur" champ d'application à utiliser; normalement, nous penserions à 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 d'effacer le cache, pour garantir que les valeurs périmées n'interfèrent pas avec notre sémantique?

Si exchangeRate utilisait var, cela 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 const, nous étendons la "portée" à une exécution du programme: il serait prudent de mettre en cache les valeurs de retour de dollarToEuro jusqu'à 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 à travers exécutions (par exemple, enregistrer des données dans un fichier, puis les charger dans une autre courir). 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 le 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 fonctionnerait même à travers 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 à l'autre bout du monde qui a utilisé une fonction identique il y a des années sur un projet différent.

7
Warbo

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.

4
11112222233333

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 noté, "cela pourrait me donner une sortie différente demain" . Si tel était le cas, la réponse serait un "non" retentissant . Cela est particulièrement vrai si votre comportement prévu de dollarToEuro a é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 dollarToEuro sur la fonction fetchFromDatabase. Par conséquent, remaniez la définition de dollarToEuro comme suit:

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

Avec ce résultat, étant donné la prémisse que fetchFromDatabase fonctionne de manière satisfaisante, alors nous pouvons conclure que la projection de fetchFromDatabase sur dollarToEuro doit être satisfaisante. Ou l'instruction "fetchFromDatabase est pur" implique dollarToEuro est pur (puisque fetchFromDatabase est une base pour dollarToEuro par le facteur scalaire de x.

D'après le message d'origine, je peux comprendre que fetchFromDatabase est un temps de fonction. Améliorons l'effort de refactorisation pour rendre cette compréhension transparente, qualifiant donc clairement fetchFromDatabase de 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, dollarToEuro peut être testé à l'unité en prouvant simplement qu'il appelle correctement fetchFromDatabase (ou son dérivé exchangeRate).

2
Igwe Kalu