Disons que fn(x)
est une fonction pure qui fait quelque chose de cher, comme renvoyer une liste des facteurs premiers de x
.
Et disons que nous faisons une version mémorisée de la même fonction appelée memoizedFn(x)
. Il renvoie toujours le même résultat pour une entrée donnée, mais il conserve un cache privé des résultats précédents pour améliorer les performances.
Formellement parlant, memoizedFn(x)
est-il considéré comme pur?
Ou existe-t-il un autre nom ou terme de qualification utilisé pour faire référence à une telle fonction dans les discussions FP? les valeurs de retour.)
Oui. La version mémorisée d'une fonction pure est également une fonction pure.
Tout ce qui importe à la pureté de la fonction, c'est l'effet que les paramètres d'entrée sur la valeur de retour de la fonction (passer la même entrée devrait toujours produire la même sortie) et tous les effets secondaires pertinents pour les états globaux (par exemple, texte vers le terminal ou l'interface utilisateur ou le réseau) . Le temps de calcul et les utilisations de mémoire supplémentaire ne sont pas pertinents pour la pureté de la fonction.
Les caches d'une fonction pure sont à peu près invisibles pour le programme; un langage de programmation fonctionnel est autorisé à optimiser automatiquement une fonction pure en une version mémorisée de la fonction s'il peut déterminer qu'il sera avantageux de le faire. En pratique, déterminer automatiquement quand la mémorisation est bénéfique est en fait un problème assez difficile, mais une telle optimisation serait valide.
Wikipedia définit un "Pure Function" comme une fonction qui a les propriétés suivantes:
Sa valeur de retour est la même pour les mêmes arguments (pas de variation avec des variables statiques locales, des variables non locales, des arguments de référence mutables ou des flux d'entrée d'I/O appareils).
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 effet, une fonction pure retourne la même sortie avec la même entrée, et n'affecte rien d'autre en dehors de la fonction. Pour des raisons de pureté, peu importe comment la fonction calcule sa valeur de retour, tant qu'il renvoie la même sortie pour la même entrée.
Langages fonctionnellement purs comme Haskell tilisez régulièrement la mémorisation pour accélérer une fonction en mettant en cache ses résultats précédemment calculés.
Oui, les fonctions pures mémorisées sont communément appelées pures. Cela est particulièrement courant dans des langages comme Haskell, dans lesquels les résultats mémorisés, évalués paresseusement et immuables sont une fonctionnalité intégrée.
Il y a une mise en garde importante: la fonction de mémorisation doit être thread-safe, ou bien vous pourriez obtenir une condition de concurrence critique lorsque deux threads essaient tous les deux de l'appeler.
Un exemple d'un informaticien utilisant le terme "purement fonctionnel" de cette façon est ce billet de blog de Conal Elliott sur la mémorisation automatique:
De façon surprenante, la mémorisation peut être implémentée simplement et purement fonctionnellement dans un langage fonctionnel paresseux.
Il existe de nombreux exemples dans la littérature évaluée par des pairs, et ce depuis des décennies. Par exemple, cet article de 1995, "Utilisation de la mémorisation automatique comme outil d'ingénierie logicielle dans les systèmes d'IA réels" utilise un langage très similaire dans la section 5.2 pour décrire ce que nous appellerions aujourd'hui une fonction pure:
La mémorisation ne fonctionne que pour les vraies fonctions, pas les procédures. Autrement dit, si le résultat d'une fonction n'est pas spécifié de manière complète et déterministe par ses paramètres d'entrée, l'utilisation de la mémorisation donnera des résultats incorrects. Le nombre de fonctions pouvant être mémorisées avec succès sera augmenté en encourageant l'utilisation d'un style de programmation fonctionnel dans tout le système.
Certaines langues impératives ont un idiome similaire. Par exemple, un static const
la variable en C++ n'est initialisée qu'une seule fois, avant que sa valeur ne soit utilisée, et ne mute jamais.
Cela dépend de la façon dont vous le faites.
Habituellement, les gens veulent mémoriser en mutant une sorte de dictionnaire de cache. Cela a tous les problèmes associés à la mutation impure, tels que devoir s'inquiéter de la concurrence, craindre que le cache ne devienne trop volumineux, etc.
Cependant, vous pouvez mémoriser sans mutation de mémoire impure. Un exemple se trouve dans cette réponse , où je fais le suivi des valeurs mémorisées en externe au moyen d'un argument lengths
.
Dans le lien fourni par Robert Harvey , une évaluation paresseuse est utilisée pour éviter les effets secondaires.
Une autre technique parfois utilisée consiste à marquer explicitement la mémorisation comme un effet secondaire impur dans le contexte d'un type IO
, comme avec la fonction de mémorisation de chats-effect .
Ce dernier soulève le point que parfois le but est simplement d'encapsuler une mutation plutôt que de l'éliminer. La plupart des programmeurs fonctionnels le considèrent "assez pur" pour rendre l'impureté explicite et encapsulée.
Si vous voulez qu'un terme le différencie d'une fonction vraiment pure, je pense qu'il suffit de dire "mémorisé avec un dictionnaire mutable". Cela permet aux gens de savoir comment l'utiliser en toute sécurité.
Habituellement, une fonction qui retourne une liste n'est pas pure du tout car elle nécessite une allocation de stockage et peut ainsi échouer (par exemple en lançant une exception, qui n'est pas pure). Un langage qui a des types de valeur et peut représenter une liste en tant que type de valeur de taille limitée peut ne pas avoir ce problème. Pour cette raison, votre exemple n'est probablement pas pur.
En général, si la mémorisation peut être effectuée d'une manière sans défaillance (par exemple en ayant un stockage alloué statiquement pour les résultats mémorisés et une synchronisation interne pour contrôler l'accès à ceux-ci si la langue admet les threads), il est raisonnable d'envisager une telle fonction pur.
Vous pouvez implémenter la mémorisation sans effets secondaires en utilisant state monad .
[State monad] est fondamentalement une fonction S => (S, A), où S est le type qui représente votre état et A est le résultat que la fonction produit - Cats State .
Dans votre cas, l'état serait la valeur mémorisée ou rien (c'est-à-dire Haskell Maybe
ou Scala Option[A]
). Si la valeur mémorisée est présente, elle est renvoyée sous la forme A
, sinon A
est calculée et renvoyée à la fois comme état de transition et comme résultat.