Sprache
Mutationen und Updates: Deep-Set, Map-Update und mut
Dieses Kapitel bündelt die Stellen, an denen Antra Werte bewusst verändert — und die Alternativen, die stattdessen neue Werte bauen. Das Gegenstück zum Verändern, das Auseinanderziehen per Destructuring, steht jetzt bei den Patterns in Kapitel 07.
Deep-Set mit :=#
:= ist die bewusst tiefe Mutationsform:
my config = %{}
config.db.host := "localhost"
config.db.port := 5432Regeln:
- die Root-Bindung muss existieren
- die Root-Bindung muss mutierbar sein
- fehlende Zwischenstufen werden erzeugt
- Array-Schritte und Map-Schritte können gemischt werden
Stilistisch gilt fuer User-Code jetzt die einfache Klammerregel:
obj.namefuer statische Propertiesobj[key]fuer dynamische Keys und Index-Zugriffeobj?.name,obj?.[key]undarr?[i]fuer sichere Variantenobj[key] := valueundarr[i] := valuefuer Schreibzugriffe in Container
my m = %{}
m.items[0].name := "Ada"String-Index-Zuweisung#
Ein einzelnes Zeichen eines Strings kannst du per Index setzen:
my s = "Felix"
s[0] = "K"
dd(s) # "Kelix"Weil Strings unveränderlich sind, ist das kein In-Place-Set, sondern ein Rebind der Variablen auf einen neuen String. Daraus folgen die Regeln:
- die rechte Seite muss genau ein Zeichen sein (
s[0] = "XY"ist ein Fehler) - der Index muss existieren; out-of-bounds ist ein Fehler (kein Auffüllen)
- negative Indizes zählen von hinten (
s[-1] = "!") - das Ziel muss mutierbar sein (
my-Variable bzw. ein veränderbarer Container);val/const→ Fehler - funktioniert auch für einen String in einem Container
(
obj.name[0] = …,xs[0][0] = …)
In Kombination mit einem mut-Parameter wirkt die Änderung zum Aufrufer zurück
(siehe Kapitel 8, „mut bei Parametern").
Was := genau erzeugt#
:= darf entlang des Pfads fehlende Knoten anlegen. Das gilt nicht nur fuer fehlende Map-Felder, sondern auch fuer fehlende Array-Pfade und das eigentliche Ziel an der letzten Stelle.
Die praktische Heuristik ist einfach:
- wenn der naechste Schritt wie ein Property- oder Key-Zugriff aussieht, wird eine Map angelegt
- wenn der naechste Schritt wie ein Index-Zugriff aussieht, wird ein Array angelegt
Beispiele:
my data = %{}
data.user.name := "Ada"
data.users[0].name := "Bob"
data.users[0].tags[1] := :active
data.settings[:theme] := "light"Dabei passiert sinngemaess Folgendes:
data.userwird als Map angelegtdata.userswird als Array angelegtdata.users[0]wird als Map angelegtdata.users[0].tagswird als Array angelegtdata.settingswird als Map angelegt
Entscheidend ist: du musst diese Zwischenstrukturen nicht vorher manuell bauen, solange die Root-Bindung selbst existiert und mutierbar ist.
Das Ziel am Ende darf ebenfalls vorher fehlen. Sowohl ein neuer Key als auch ein bisher nicht gesetzter Index koennen also durch := erstmals befuellt werden.
my cfg = %{}
cfg["host"] := "localhost"
my xs = []
xs[0] := 10Die Root-Bindung selbst wird dabei nicht implizit neu deklariert. := braucht also weiterhin einen vorhandenen Namen links vom Pfad.
Auf const-Bindings ist := nicht erlaubt:
const cfg = %{ port: 3000 }
cfg.port := 8080 # FehlerUnterschied zwischen = und :=#
user.name = "Ada"
user.profile.city := "Berlin"Merke dir:
=schreibt nur in ein Ziel, das schon da ist:=navigiert einen Pfad und darf fehlende Map- oder Array-Knoten unterwegs erzeugen
Mehrere Namen auf einmal: [a, b] = ...#
Steht links von = ein Array aus Namen, bekommen alle zugleich einen neuen
Wert. Die rechte Seite wird vorher vollständig ausgewertet — so tauscht man
zwei Werte ohne Hilfsvariable:
my a = 1
my b = 2
[a, b] = [b, a] # a = 2, b = 1
my x = 0
my y = 1
for _i <- 1..10 { [x, y] = [y, x + y] }
println(x) # 55- Die Namen müssen schon gebunden und veränderlich sein (
my);val/constbleiben unveränderlich, neue Namen legt die Form nicht an. - Rechts muss ein Array mit genau so vielen Werten stehen.
_überspringt eine Position,[a, [b, _]]geht auch verschachtelt.- Nur Namen als Ziele: Properties und Indizes (
[p.x, xs[0]] = ...) gehen einzeln mit=.
Map-Update ohne Mutation#
Wenn du keine in-place Mutation willst, nimm Map-Updates mit |:
my updated = %{user | age: 37}Das ist näher an funktionalem Stil:
- alter Wert bleibt erhalten
- neuer Wert wird erzeugt
Auch Tagged Maps werden dabei sauber weitergetragen:
my updated = %{existing_user | age: 37}
my explicit = %User{raw_map | age: 37}Warum gibt es beides?#
Weil beide Formen legitim sind, aber unterschiedliche Absichten ausdrücken:
- Map-Update: "ich baue einen neuen Wert"
:=: "ich mutiere gezielt einen vorhandenen Wert"
Parameter binden per Default wie val#
Ein Funktionsparameter ist standardmäßig nicht reassignbar. Ein plainer
Parameter verhält sich also wie val: du kannst ihn nicht neu zuweisen, den
darunterliegenden Wert aber sehr wohl per Deep-Set verändern.
fn f(x) -> { x = 9 } # Fehler: Parameter bindet wie val
fn f(xs) -> { xs[0] = 9 } # ok: Deep-Set ist erlaubt (nur die Bindung ist fix)Wenn du den Parameter im Körper wirklich reassignen willst, ist my der
explizite Opt-out:
fn f(my x) -> { x = x + 1; x } # ok: my macht die Bindung reassignbarDas gilt auch für destrukturierende Parameter — my steht dann vor dem Pattern:
fn kw(my [a: c, b: ]) -> { c = 10 } # c ist reassignbarKurz die vier Formen im Vergleich:
| Form | Reassign (x = …) | Deep-Set (x[..] = …) |
|---|---|---|
x (Default) / val x | Fehler | erlaubt |
my x | erlaubt | erlaubt |
const x | Fehler | Fehler (Wert eingefroren) |
mut x | erlaubt, wirkt auf den Aufrufer zurück | erlaubt, wirkt zurück |
Warum dieser Default? Er passt zur Grundhaltung dieses Kapitels — standardmäßig
lieber neue Werte bauen, Veränderung nur dort, wo sie ausdrücklich gewollt ist.
my, mut und := sind die sichtbaren Signale dafür.
mut in Funktionsparametern#
Wenn eine Funktion ihr Composite-Argument verändern soll, wird das explizit markiert:
fn touch(mut cfg) -> cfg.port := 8080Das ist absichtlich sichtbarer als in vielen Mainstream-Sprachen.
val und const unterscheiden sich hier bewusst:
valdarf anmut-Parameter uebergeben werden (Bindung ist fix, Wert aber mutierbar).constdarf nicht anmut-Parameter uebergeben werden (Wert ist eingefroren).
fn touch(mut cfg) -> cfg.port := 8080
val cfg1 = %{ port: 3000 }
touch(cfg1) # erlaubt
const cfg2 = %{ port: 3000 }
touch(cfg2) # FehlerWichtig: mut macht aus einem const-Argument kein mutierbares Argument. Das hier ist also ebenfalls ein Fehler:
fn touch(mut cfg) -> cfg.port := 8080
const cfg = %{ port: 3000 }
touch(cfg) # FehlerAls kurze Abgrenzung: mut ist nicht derselbe Modus wie on_use.
mutsagt: diese Funktion darf den uebergebenen Composite-Wert veraendernon_usesagt: dieser Ausdruck soll erst spaeter im Funktionskoerper ausgewertet werden
Beispiel fuer on_use:
fn assert_error(on_use expr, needle) -> ...on_use ist also kein Mutationsmarker, sondern ein Evaluationsmarker fuer
Parameter. Beide Modi duerfen in v1 nicht auf demselben Parameter kombiniert
werden.
as mut mit Patterns#
Oft willst du nicht nur mutieren, sondern gleichzeitig eine Form verlangen:
fn enrich(%User{..} as mut user) -> user.id := 42Das heißt:
- der Parameter muss zu
%User{..}passen - zusätzlich wird er als
usergebunden - über diesen Alias darf in-place mutiert werden
Auch hier gilt: ein const-Wert darf nicht ueber as mut in einen mutierbaren Alias umgebogen werden.
Das funktioniert auch für Tagged Arrays und Tagged Maps.
Ist mut schneller als einen neuen Wert zurueckgeben?#
Meistens nicht. Die naheliegende Annahme ist, dass mut das Kopieren spart --
aber Antra kopiert Argumente ohnehin nie. Composites werden immer als
Referenz uebergeben, mit oder ohne mut. Immutability wird nicht durch Kopieren
erzwungen, sondern durch eine Registrierung eingefrorener Werte, die bei jeder
Mutation befragt wird.
mut spart also nichts beim Uebergeben. Es kostet sogar etwas extra: einen
Frozen-Check beim Aufruf und einen Copy-out-Durchlauf beim Verlassen der
Funktion, der reassignte Parameter in die Aufrufer-Variable zurueckschreibt.
Was mut wirklich aendert, ist die Komplexitaet beim Akkumulieren. Ein
xs ++ [v] baut jedes Mal ein frisches Array -- in einer Schleife wird daraus
O(n^2). Array.push auf einem mut-Parameter haengt in-place an und bleibt
O(n).
Gemessen an einer Schleife, die n Elemente sammelt:
| n | mut + push | ++ und neu zurueckgeben |
|---|---|---|
| 2.000 | 0,44 s | 0,28 s |
| 8.000 | 1,49 s | 1,38 s |
| 16.000 | 2,90 s | 3,72 s |
| 32.000 | 5,77 s | 11,40 s |
Bis rund 10.000 Elementen gewinnt die immutable Variante, weil das Neubauen
eines Arrays ein einziger schneller Kopiervorgang ist, waehrend jeder
mut-Aufruf Frozen-Check, Dispatch und Copy-out bezahlt. Erst darueber schlaegt
die Asymptotik durch.
Praktische Regel: nimm die immutable Variante, ausser du akkumulierst
wirklich grosse Collections in einer Schleife. Und wenn du dich fuer mut
entscheidest, dann wegen der Komplexitaet, nicht wegen ersparter Kopien -- die
gab es nie.
Anfängerregel für Mutation#
Wenn du unsicher bist, fang mit dem einfacheren mentalen Modell an:
- standardmäßig lieber neue Werte bauen
- Mutation nur dort einsetzen, wo sie die API wirklich klarer macht
mutund:=als Signal lesen: hier wird absichtlich verändert
So bleibt der Code leichter zu verstehen.
Referenz: := und Merge-Varianten#
Weitere Deep-Set-Fälle mit dynamischen Keys:
my k = "host"
config.db[k] := "staging.example.com"
my section = "db"
config[section].port := 3306Und ebenso fuer gemischte Ketten:
my i = 0
config.users[i].profile["city"] := "Wien"Hier gilt dieselbe Regel wie oben: users wird bei Bedarf als Array erzeugt, users[i] als Map, profile wieder als Map und das Feld "city" am Ende direkt gesetzt.
Map-Update ergänzt sich mit flachem und tiefem Merge über Spread:
my ext = %{ ..base, z: 3 }
my merged = %{ ..base, ..ext }
my deep = %{ ...nested1, ...nested2 }Beide Formen tun Ähnliches, meinen aber Verschiedenes:
%{base | key: value}aktualisiert einen Wert. Genau eine Basis links vom|—%{a | b}ist ein Parse-Fehler — und der Tag der Basis bleibt erhalten.%{self | age: 31}in einer Package-Methode ist damit weiter ein%Person{…}. Die Basis darf ein Ausdruck sein:%{state.player | x: 0}.%{..a, ..b}baut ein neues Literal aus beliebig vielen Quellen. Den Tag bestimmt hier die äußere Form:%Point{..a}ist getaggt,%{..a}nicht.
Als Spread-Quelle sind Maps und getaggte Maps erlaubt (bei letzteren werden die Daten gespreadet). Alles andere meldet sich deutlich:
Error: spread in a map literal requires a map or tagged map; got ArrayAusführlicher steht das im Collections-Kapitel unter Spread und Deep Merge.
