Zijn Elixir-variabelen echt onveranderlijk?

In Dave Thomas’s boek Programming Elixir zegt hij “Elixir dwingt onveranderlijke gegevens af” en zegt verder:

In Elixir, als een variabele eenmaal verwijst naar een lijst zoals [1,2,3], weet je dat deze altijd naar dezelfde waarden zal verwijzen (totdat je de variabele opnieuw bindt).

Dit klinkt als “het zal nooit veranderen tenzij je het verandert”, dus ik weet niet wat het verschil is tussen veranderlijkheid en opnieuw binden. Een voorbeeld waarin de verschillen worden benadrukt, zou erg nuttig zijn.


Antwoord 1, autoriteit 100%

Denk niet aan ‘variabelen’ in Elixir als variabelen in imperatieve talen, ‘spaties voor waarden’. Zie ze eerder als “labels voor waarden”.

Misschien zou je het beter begrijpen als je kijkt naar hoe variabelen (“labels”) werken in Erlang. Telkens wanneer u een “label” aan een waarde koppelt, blijft het er voor altijd aan gebonden (hier gelden uiteraard scope-regels).

In Erlang kun je nietdit schrijven:

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)

in plaats daarvan moet je dit schrijven:

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"

Zoals je kunt zien is dit erg onhandig, vooral voor het herstructureren van code. Als u een nieuwe regel na de eerste regel wilt invoegen, moet u alle v* opnieuw nummeren of iets schrijven als “v1a = …”

Dus in Elixir kun je variabelen opnieuw binden (de betekenis van het “label wijzigen”), voornamelijk voor je gemak:

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"

Samenvatting:In imperatieve talen zijn variabelen als koffers met de naam: je hebt een koffer met de naam “v”. Eerst doe je er boterham in. Dan doe je er een appel in (het broodje is verloren gegaan en misschien opgegeten door de vuilnisman). In Erlang en Elixir is de variabele niet een plaatsom iets in te zetten. Het is gewoon een naam/labelvoor een waarde. In Elixir kunt u een betekenis van het label wijzigen. In Erlang kan dat niet. Dat is de reden waarom het geen zin heeft om “geheugen toe te wijzen voor een variabele” in Erlang of Elixir, omdat variabelen geen ruimte innemen. Waarden wel.Nu zie je misschien duidelijk het verschil.

Als je dieper wilt graven:

1) Kijk hoe “ongebonden” en “gebonden” variabelen werken in Prolog. Dit is de bron van dit misschien ietwat vreemde Erlang-concept van “variabelen die niet variëren”.

2) Merk op dat “=” in Erlang echt geen toewijzingsoperator is, het is gewoon een overeenkomstoperator! Wanneer u een ongebonden variabele koppelt aan een waarde, bindt u de variabele aan die waarde. Het matchen van een gebonden variabele is net als het matchen van een waarde waaraan het is gebonden. Dit levert dus een overeenkomst-fout op:

v = 1,
v = 2,   % in fact this is matching: 1 = 2

3) Bij Elixir is dat niet het geval. Dus in Elixir moet er een speciale syntaxis zijn om matching te forceren:

v = 1
v = 2   # rebinding variable to 2
^v = 3  # matching: 2 = 3 -> error

Antwoord 2, autoriteit 63%

Onveranderlijkheid betekent dat gegevensstructuren niet veranderen. De functie HashSet.newretourneert bijvoorbeeld een lege set en zolang je de verwijzing naar die set vasthoudt, zal deze nooit niet-leeg worden. Wat je echter kuntin Elixir is een variabele verwijzing naar iets weggooien en deze opnieuw aan een nieuwe verwijzing binden. Bijvoorbeeld:

s = HashSet.new
s = HashSet.put(s, :element)
s # => #HashSet<[:element]>

Wat nietkan gebeuren, is dat de waarde onder die verwijzing verandert zonder dat u deze expliciet opnieuw verbindt:

s = HashSet.new
ImpossibleModule.impossible_function(s)
s # => #HashSet<[:element]> will never be returned, instead you always get #HashSet<[]>

Vergelijk dit met Ruby, waar je zoiets als het volgende kunt doen:

s = Set.new
s.add(:element)
s # => #<Set: {:element}>

Antwoord 3, autoriteit 40%

Erlang en uiteraard Elixir dat erop is gebouwd, omarmt onveranderlijkheid.
Ze staan ​​eenvoudigweg niet toe dat waarden in een bepaalde geheugenlocatie worden gewijzigd.Nooit totdat de variabele wordt verzameld of buiten bereik is.

Variabelen zijn niet het onveranderlijke. De gegevens waarnaar ze verwijzen, zijn onveranderlijk. Daarom wordt het wijzigen van een variabele rebinding genoemd.

Je richt het op iets anders, en verandert niet waar het naar verwijst.

x = 1gevolgd door x = 2verandert de gegevens die zijn opgeslagen in het computergeheugen waar de 1 was niet in een 2. Het zet een 2 in een nieuwe plaats en wijst xernaar.

xis slechts toegankelijk voor één proces tegelijk, dus dit heeft geen invloed op gelijktijdigheid en gelijktijdigheid is de belangrijkste plaats om te zorgen dat iets toch onveranderlijk is.

Opnieuw binden verandert de status van een object helemaal niet, de waarde bevindt zich nog steeds op dezelfde geheugenlocatie, maar het label (variabele) wijst nu naar een andere geheugenlocatie, zodat onveranderlijkheid behouden blijft. Rebinding is niet beschikbaar in Erlang, maar hoewel het in Elixir is, remt dit geen enkele beperking die wordt opgelegd door de Erlang VM, dankzij de implementatie ervan.
De redenen achter deze keuze worden goed uitgelegd door Josè Valim in deze kern.

Stel dat je een lijst had

l = [1, 2, 3]

en je had een ander proces dat lijsten nam en er vervolgens herhaaldelijk “dingen” tegen uitvoerde en ze tijdens dit proces zou veranderen, zou slecht zijn. Je zou die lijst kunnen sturen zoals

send(worker, {:dostuff, l})

Nu wil je volgende stukje code misschien l bijwerken met meer waarden voor verder werk dat niets te maken heeft met wat dat andere proces doet.

l = l ++ [4, 5, 6]

Oh nee, nu gaat dat eerste proces ongedefinieerd gedrag vertonen omdat je de lijst hebt gewijzigd, toch? Mis.

Die originele lijst blijft ongewijzigd. Wat je echt deed, was een nieuwe lijst maken op basis van de oude en ik opnieuw aan die nieuwe lijst binden.

Het afzonderlijke proces heeft nooit toegang tot l. De gegevens waarnaar ik oorspronkelijk verwees, zijn ongewijzigd en het andere proces (vermoedelijk, tenzij het het negeerde) heeft zijn eigen afzonderlijke verwijzing naar die originele lijst.

Het gaat erom dat u geen gegevens tussen processen kunt delen en deze vervolgens kunt wijzigen terwijl een ander proces ernaar kijkt. In een taal als Java, waar je een aantal veranderlijke typen hebt (alle primitieve typen plus verwijzingen zelf) zou het mogelijk zijn om een ​​structuur/object te delen dat een int bevatte en die int van de ene thread te veranderen terwijl een andere het aan het lezen was.

In feite is het mogelijk om een ​​groot geheel getal type in Java gedeeltelijk te wijzigen terwijl het door een andere thread wordt gelezen. Of tenminste, dat was het vroeger, niet zeker of ze dat aspect van de dingen vastklemden met de 64-bits overgang. Hoe dan ook, het punt is dat je het kleed onder andere processen/threads vandaan kunt halen door gegevens te veranderen op een plek waar beide tegelijk naar kijken.

Dat is niet mogelijk in Erlang en bij uitbreiding Elixir. Dat is wat onveranderlijkheid hier betekent.

Om iets specifieker te zijn, in Erlang (de oorspronkelijke taal voor de VM Elixir draait op) was alles onveranderlijke variabelen met een enkele toewijzing en Elixir verbergt een patroon dat Erlang-programmeurs hebben ontwikkeld om dit te omzeilen.

In Erlang, als a=3, dan was dat wat a de waarde zou zijn voor de duur van het bestaan ​​van die variabele totdat deze buiten het bereik viel en als afval werd verzameld.

Dit was soms handig (er verandert niets na toewijzing of patroonovereenkomst, dus het is gemakkelijk om te redeneren wat een functie doet), maar ook een beetje omslachtig als je meerdere dingen met een variabele of verzameling zou doen tijdens het uitvoeren van een functie.

Code ziet er vaak zo uit:

A=input, 
A1=do_something(A), 
A2=do_something_else(A1), 
A3=more_of_the_same(A2)

Dit was een beetje onhandig en maakte refactoring moeilijker dan nodig was. Elixir doet dit achter de schermen, maar verbergt het voor de programmeur via macro’s en codetransformaties die door de compiler worden uitgevoerd.

Geweldige discussie hier

immutability-in-elixir


Antwoord 4, autoriteit 4%

De variabelen zijn in feite onveranderlijk, elke nieuwe herbinding (toewijzing) is alleen zichtbaar voor toegang die daarna komt. Alle eerdere toegangen, verwijzen nog steeds naar oude waarde(n) op het moment van hun oproep.

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

Antwoord 5

Om het heel eenvoudig te maken

variabelen in elixer zijn niet zoals een container waar je items uit de container blijft toevoegen en verwijderen of wijzigen.

In plaats daarvan zijn ze als labels die aan een container zijn gekoppeld. Wanneer u een variabele opnieuw toewijst, is het zo eenvoudig als u een label uit de ene container kiest en deze op een nieuwe container plaatst met de verwachte gegevens erin.

Other episodes