Dans le livre de Dave Thomas, Programming Elixir, il déclare "Elixir applique des données immuables" et poursuit:
Dans Elixir, une fois qu'une variable fait référence à une liste telle que [1,2,3], vous savez qu'elle fera toujours référence à ces mêmes valeurs (jusqu'à ce que vous reliez la variable).
Cela ressemble à "cela ne changera jamais à moins que vous ne le changiez", donc je suis confus quant à la différence entre la mutabilité et la reliure. Un exemple mettant en évidence les différences serait vraiment utile.
L'immuabilité signifie que les structures de données ne changent pas. Par exemple, la fonction HashSet.new
renvoie un ensemble vide et tant que vous gardez la référence à cet ensemble, il ne deviendra jamais non vide. Ce que vous pouvez faire dans Elixir est de jeter une référence variable à quelque chose et de la lier à une nouvelle référence. Par exemple:
s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>
Ce qui ne peut pas se produire est la valeur sous cette référence qui change sans que vous la reliez explicitement:
s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>
Comparez cela à Ruby, où vous pouvez faire quelque chose comme ceci:
s = Set.new
s.add(:element)
s # => #<Set: {:element}>
Ne considérez pas les "variables" dans Elixir comme des variables dans les langages impératifs, des "espaces pour les valeurs". Considérez-les plutôt comme des "étiquettes de valeurs".
Peut-être que vous feriez mieux de le comprendre lorsque vous regardez comment les variables ("étiquettes") fonctionnent dans Erlang. Chaque fois que vous liez un "label" à une valeur, il y reste lié pour toujours (les règles de portée s'appliquent ici bien sûr).
En Erlang, vous ne pouvez pas écrire ceci:
v = 1, % value "1" is now "labelled" "v"
% wherever you write "1", you can write "v" and vice versa
% the "label" and its value are interchangeable
v = v+1, % you can not change the label (rebind it)
v = v*10, % you can not change the label (rebind it)
à la place, vous devez écrire ceci:
v1 = 1, % value "1" is now labelled "v1"
v2 = v1+1, % value "2" is now labelled "v2"
v3 = v2*10, % value "20" is now labelled "v3"
Comme vous pouvez le voir, cela est très gênant, principalement pour la refactorisation de code. Si vous souhaitez insérer une nouvelle ligne après la première ligne, vous devrez renuméroter tous les v * ou écrire quelque chose comme "v1a = ..."
Ainsi, dans Elixir, vous pouvez lier des variables (changer la signification du "label"), principalement pour votre commodité:
v = 1 # value "1" is now labelled "v"
v = v+1 # label "v" is changed: now "2" is labelled "v"
v = v*10 # value "20" is now labelled "v"
Résumé: Dans les langages impératifs, les variables sont comme des valises nommées: vous avez une valise nommée "v". Au début, vous y mettez du sandwich. Que vous mettez un Apple dedans (le sandwich est perdu et peut-être mangé par le ramasse-miettes). Dans Erlang et Elixir, la variable n'est pas n endroit pour mettre quelque chose dedans. C'est juste n nom/étiquette pour une valeur. Dans Elixir vous pouvez changer la signification de l'étiquette. Dans Erlang vous ne pouvez pas. C'est la raison pour laquelle cela n'a pas de sens "allouer de la mémoire pour une variable" dans Erlang ou Elixir, car les variables n'occupent pas d'espace. Les valeurs le font. Maintenant, vous voyez peut-être clairement la différence.
Si vous voulez creuser plus profondément:
1) Regardez comment les variables "non liées" et "liées" fonctionnent dans Prolog. C'est la source de ce concept Erlang peut-être un peu étrange de "variables qui ne varient pas".
2) Notez que "=" dans Erlang n'est vraiment pas un opérateur d'affectation, c'est juste un opérateur de correspondance! Lorsque vous faites correspondre une variable non liée avec une valeur, vous liez la variable à cette valeur. Faire correspondre une variable liée, c'est comme faire correspondre une valeur à laquelle elle est liée. Donc, cela produira une erreur de correspondance :
v = 1,
v = 2, % in fact this is matching: 1 = 2
3) Ce n'est pas le cas dans Elixir. Donc, dans Elixir, il doit y avoir une syntaxe spéciale pour forcer la correspondance:
v = 1
v = 2 # rebinding variable to 2
^v = 3 # matching: 2 = 3 -> error
Erlang et évidemment Elixir qui est construit dessus, embrasse l'immuabilité. Ils ne permettent tout simplement pas de modifier les valeurs d'un certain emplacement de mémoire. Jamais Jusqu'à ce que la variable soit récupérée ou hors de portée.
Les variables ne sont pas immuables. Les données sur lesquelles ils pointent sont immuables. C'est pourquoi la modification d'une variable est appelée reliure.
Vous pointez sur autre chose, ne changez pas la chose vers laquelle il pointe.
x = 1
suivi par x = 2
ne change pas les données stockées dans la mémoire de l'ordinateur où se trouvait le 1 en 2. Il place un 2 dans un nouvel emplacement et pointe x
vers lui.
x
n'est accessible que par un processus à la fois, cela n'a donc aucun impact sur la concurrence et la concurrence est le principal endroit où se soucier même si quelque chose est immuable de toute façon.
La reliure ne modifie pas du tout l'état d'un objet, la valeur est toujours au même emplacement de mémoire, mais son étiquette (variable) pointe maintenant vers un autre emplacement de mémoire, donc l'immuabilité est préservée. La reliure n'est pas disponible dans Erlang, mais tant qu'elle est dans Elixir, cela ne freine aucune contrainte imposée par la machine virtuelle Erlang, grâce à sa mise en œuvre. Les raisons de ce choix sont bien expliquées par Josè Valim dans ce Gist .
Disons que vous aviez une liste
l = [1, 2, 3]
et vous aviez un autre processus qui prenait des listes, puis effectuait des "trucs" contre eux à plusieurs reprises et les changer pendant ce processus serait mauvais. Vous pourriez envoyer cette liste comme
send(worker, {:dostuff, l})
Maintenant, votre prochain morceau de code pourrait vouloir mettre à jour l avec plus de valeurs pour un travail supplémentaire qui n'est pas lié à ce que fait l'autre processus.
l = l ++ [4, 5, 6]
Oh non, maintenant ce premier processus va avoir un comportement indéfini parce que vous avez changé la liste non? Faux.
Cette liste originale reste inchangée. Ce que vous avez vraiment fait, c'est de créer une nouvelle liste basée sur l'ancienne et de la relier à cette nouvelle liste.
Le processus séparé n'a jamais accès à l. Les données à l'origine pointées sont inchangées et l'autre processus (vraisemblablement, sauf s'il l'a ignoré) a sa propre référence distincte à cette liste d'origine.
Ce qui importe, c'est que vous ne pouvez pas partager les données entre les processus, puis les modifier pendant qu'un autre processus les examine. Dans un langage comme Java où vous avez des types mutables (tous les types primitifs plus les références eux-mêmes), il serait possible de partager une structure/un objet qui contenait disons un int et de changer cet int d'un thread tandis qu'un autre le lisait.
En fait, il est possible de changer un grand type entier dans Java partiellement pendant qu'il est lu par un autre thread. Ou du moins, c'était le cas, je ne sais pas s'ils ont limité cet aspect des choses avec la transition 64 bits. Quoi qu'il en soit, le fait est que vous pouvez retirer le tapis de sous d'autres processus/threads en modifiant les données dans un endroit que les deux regardent simultanément.
Ce n'est pas possible à Erlang et par extension Elixir. C'est ce que signifie l'immuabilité ici.
Pour être un peu plus précis, dans Erlang (la langue d'origine de VM Elixir fonctionne), tout était des variables immuables à affectation unique et Elixir cache un modèle que les programmeurs Erlang ont développé pour contourner ce problème.
À Erlang, si a = 3, alors c'est ce que va être sa valeur pendant la durée d'existence de cette variable jusqu'à ce qu'elle tombe hors de portée et soit récupérée.
Cela était parfois utile (rien ne change après l'affectation ou la correspondance de modèle, il est donc facile de raisonner sur ce qu'une fonction fait), mais aussi un peu lourd si vous faisiez plusieurs choses à une variable ou à une collection au cours du cours d'exécution d'une fonction.
Le code ressemblait souvent à ceci:
A=input,
A1=do_something(A),
A2=do_something_else(A1),
A3=more_of_the_same(A2)
C'était un peu maladroit et a rendu le refactoring plus difficile qu'il ne devait l'être. Elixir le fait dans les coulisses, mais le cache au programmeur via des macros et des transformations de code effectuées par le compilateur.
Les variables sont vraiment immuables dans le sens, chaque nouvelle liaison (affectation) n'est visible que pour l'accès qui vient après. Tous les accès précédents se réfèrent toujours aux anciennes valeurs au moment de leur appel.
foo = 1
call_1 = fn -> IO.puts(foo) end
foo = 2
call_2 = fn -> IO.puts(foo) end
foo = 3
foo = foo + 1
call_3 = fn -> IO.puts(foo) end
call_1.() #prints 1
call_2.() #prints 2
call_3.() #prints 4