Dans la programmation fonctionnelle puisque presque toutes les structures de données sont immuables, lorsque l'état doit changer, une nouvelle structure est créée. Cela signifie-t-il beaucoup plus d’utilisation de la mémoire? Je connais bien le paradigme de programmation orientée objet, maintenant j'essaie d'en apprendre davantage sur le paradigme de programmation fonctionnelle. Le concept de tout ce qui est immuable me confond. Il semblerait qu'un programme utilisant des structures immuables nécessiterait beaucoup plus de mémoire qu'un programme avec des structures mutables. Suis-je même en train de regarder cela de la bonne façon?
La seule bonne réponse à cette question est "parfois". Il existe de nombreuses astuces que les langages fonctionnels peuvent utiliser pour éviter de gaspiller la mémoire. L'immutabilité facilite le partage des données entre les fonctions, et même entre les structures de données, car le compilateur peut garantir que les données ne seront pas modifiées. Les langages fonctionnels ont tendance à encourager l'utilisation de structures de données qui peuvent être utilisées efficacement comme structures immuables (par exemple, des arbres au lieu de tables de hachage). Si vous ajoutez de la paresse dans le mélange, comme le font de nombreux langages fonctionnels, cela ajoute de nouvelles façons d'économiser de la mémoire (cela ajoute également de nouvelles façons de gaspiller la mémoire, mais je ne vais pas y aller).
Dans la programmation fonctionnelle puisque presque toutes les structures de données sont immuables, lorsque l'état doit changer, une nouvelle structure est créée. Cela signifie-t-il une utilisation beaucoup plus importante de la mémoire?
Cela dépend de la structure des données, des modifications exactes que vous avez effectuées et, dans certains cas, de l'optimiseur. À titre d'exemple, considérons l'ajout à une liste:
list2 = prepend(42, list1) // list2 is now a list that contains 42 followed
// by the elements of list1. list1 is unchanged
Ici, la mémoire supplémentaire requise est constante - tout comme le coût d'exécution de l'appel de prepend
. Pourquoi? Parce que prepend
crée simplement une nouvelle cellule qui a 42
Comme tête et list1
Comme queue. Il n'est pas nécessaire de copier ou autrement itérer sur list2
Pour y parvenir. Autrement dit, à l'exception de la mémoire requise pour stocker 42
, list2
Réutilise la même mémoire que celle utilisée par list1
. Les deux listes étant immuables, ce partage est parfaitement sécurisé.
De même, lorsque vous travaillez avec des structures arborescentes équilibrées, la plupart des opérations ne nécessitent qu'une quantité logarithmique d'espace supplémentaire car tout, mais un seul chemin de l'arborescence peut être partagé.
Pour les tableaux, la situation est un peu différente. C'est pourquoi, dans de nombreux FP langages, les tableaux ne sont pas couramment utilisés. Cependant, si vous faites quelque chose comme arr2 = map(f, arr1)
et arr1
N'est plus jamais utilisé après cette ligne, un optimiseur intelligent peut réellement créer du code qui mute arr1
au lieu de créer un nouveau tableau (sans affecter le comportement du programme). Dans ce cas, les performances seront comme dans un langage impératif bien sûr.
Les implémentations naïves exposeraient en effet ce problème - lorsque vous créez une nouvelle structure de données au lieu de mettre à jour une structure existante sur place, vous devez avoir une surcharge.
Différentes langues ont différentes manières de gérer cela, et il y a quelques astuces que la plupart utilisent.
Une stratégie est garbage collection. Au moment où la nouvelle structure a été créée, ou peu de temps après, les références à l'ancienne structure sont hors de portée, et le garbage collector la récupérera instantanément ou assez tôt, selon l'algorithme GC. Cela signifie que bien qu'il y ait encore des frais généraux, ils ne sont que temporaires et ne croîtront pas linéairement avec la quantité de données.
Un autre consiste à choisir différents types de structures de données. Où les tableaux sont la structure de données de la liste de référence dans les langages impératifs (généralement enveloppés dans une sorte de conteneur de réallocation dynamique tel que std::vector
en C++), les langages fonctionnels préfèrent souvent les listes chaînées. Avec une liste liée, une opération de préfixe ("contre") peut réutiliser la liste existante comme queue de la nouvelle liste, donc tout ce qui est réellement alloué est la nouvelle tête de liste. Des stratégies similaires existent pour d'autres types de structures de données - ensembles, arbres, vous l'appelez.
Et puis il y a l'évaluation paresseuse, à la Haskell. L'idée est que les structures de données que vous créez ne sont pas entièrement créées immédiatement; au lieu de cela, ils sont stockés en tant que "thunks" (vous pouvez les considérer comme des recettes pour construire la valeur lorsque cela est nécessaire). Ce n'est que lorsque la valeur est nécessaire que le thunk est développé en une valeur réelle. Cela signifie que l'allocation de mémoire peut être différée jusqu'à ce qu'une évaluation soit nécessaire, et à ce stade, plusieurs thunks peuvent être combinés en une seule allocation de mémoire.
Je ne connais que peu de choses sur Clojure et c'est Structures de données immuables.
Clojure fournit un ensemble de listes, vecteurs, ensembles et cartes immuables. Puisqu'ils ne peuvent pas être modifiés, "ajouter" ou "supprimer" quelque chose d'une collection immuable signifie créer une nouvelle collection comme l'ancienne mais avec le changement nécessaire. La persistance est un terme utilisé pour décrire la propriété dans laquelle l'ancienne version de la collection est toujours disponible après le "changement" et que la collection conserve ses garanties de performances pour la plupart des opérations. Plus précisément, cela signifie que la nouvelle version ne peut pas être créée à l'aide d'une copie complète, car cela nécessiterait un temps linéaire. Inévitablement, les collections persistantes sont implémentées à l'aide de structures de données liées, de sorte que les nouvelles versions peuvent partager la structure avec la version précédente.
Graphiquement, nous pouvons représenter quelque chose comme ceci:
(def my-list '(1 2 3))
+---+ +---+ +---+
| 1 | ---> | 2 | ---> | 3 |
+---+ +---+ +---+
(def new-list (conj my-list 0))
+-----------------------------+
+---+ | +---+ +---+ +---+ |
| 0 | --->| | 1 | ---> | 2 | ---> | 3 | |
+---+ | +---+ +---+ +---+ |
+-----------------------------+
En plus de ce qui a été dit dans d'autres réponses, je voudrais mentionner le langage de programmation Clean, qui prend en charge les types uniques dits . Je ne connais pas ce langage mais je suppose que les types uniques supportent une sorte de "mise à jour destructrice".
En d'autres termes, alors que la sémantique de la mise à jour d'un état est que vous créez une nouvelle valeur à partir d'une ancienne en appliquant une fonction, la contrainte d'unicité peut permettre au compilateur de réutiliser les objets de données en interne car il sait que l'ancienne valeur ne sera pas référencée plus dans le programme après que la nouvelle valeur a été produite.
Pour plus de détails, voir par ex. la page d'accueil Clean et ce article wikipedia