Sprache
Strings, Arrays, Maps und andere Sammlungen
Wer aus Python oder JavaScript kommt, findet in Antra viele vertraute Formen wieder. Der Unterschied liegt eher in der Kombination mit Pattern Matching und Tagged Values.
Strings#
Strings und Interpolation#
Strings unterstützen Interpolation, Indexing und Standardoperationen wie len, split, upper oder lower.
my name = "Felix"
println("Hallo $name")
println(len(name))
println(name[0])
# => "Hallo Felix"
# => 5
# => "F"my s1 = "Hello, ${name}!"
my s2 = 'kein ${escape} hier'
my a = "Hi $name"
my b = "Alter: $user.age"
my c = "Erstes: $items[0]"
my d = "Preis: \$5"Doppelte Strings unterstützen ${expr} und die Kurzform $name, $obj.field, $arr[0]. Einfache Strings sind raw.
$obj.field liest das Feld nur, wenn obj eine Map, getaggte Map oder ein
Paket ist. Bei jedem anderen Wert bleibt der Punkt Text: mit n = 3 ist
"$n.vox" der Dateiname 3.vox. Wer ausdrücklich zugreifen will, schreibt
${obj.field}.
String-Indexing und Ausschnitte#
Einzelne Zeichen liest du per Index, auch negativ vom Ende her:
my s = "Hallo"
s[0] # => "H"
s[-1] # => "o"
s[10] # => undef
"xyz"[1] # => "y"Das Ergebnis ist entweder ein String der Länge 1 oder undef bei Out-of-Bounds.
Auch Ausschnitte funktionieren direkt auf Strings — mit denselben zwei Formen wie bei Arrays. Der Unterschied liegt im Ergebnistyp:
my s = "Hallo Welt"
s[0..4] # => ["H", "a", "l", "l", "o"] — Range: Zeichen-Array, inklusiv
s[0:5] # => "Hallo" — Slice: String, end exklusiv
s[6:] # => "Welt"
s[:5] # => "Hallo"
s[-4:] # => "Welt"Willst du also ein Teilstueck als String weiterverwenden, nimm den Slice
s[a:b]; willst du zeichenweise arbeiten (mappen, filtern), liefert der
Range-Subscript s[a..b] direkt das Zeichen-Array. Die vollstaendigen
Slice-Regeln (offene Grenzen, negative Indizes, Klemmen bei Out-of-Bounds)
stehen unter
Slices und Range-Subscripts.
Arrays und Ranges#
Arrays#
Arrays sind geordnete Listen:
my xs = [1, 2, 3]
println(xs[0])
println(xs[1..2])
# => 1
# => [2, 3]Trailing Comma: Hinter dem letzten Element einer kommagetrennten Liste darf ein Komma stehen - in Literalen (
[1, 2, 3,],%{a: 1,},%<1, 2,>,[a: 1,]) ebenso wie in Funktionsaufrufen (f(1,)), Parameterlisten (fn f(x,),|x,|), Patterns (fn f([x,]),%{a: x,},[a: = 1,]), Destructuring (val [a, b,] = ...) und Subscripts (arr[0,]). Praktisch fuer mehrzeilige Listen und Diffs: eine Zeile kommt dazu, ohne die vorherige anzufassen. Ein leeres Element bleibt ueberall ein Fehler -[1,, 2],f(1,,2),[,1].
Typische Built-ins oder Stdlib-Helfer arbeiten direkt auf Arrays:
map(xs, |x| -> x * 2)
filter(xs, |x| -> x > 1)
# => [2, 4, 6]
# => [2, 3]Ein paar gaengige Helfer aus Builtins und Stdlib:
head(xs)
head!(xs) # wirft bei leerem Array statt undef zu liefern
tail(xs)
tail!(xs)
concat([1], [2, 3])
len(xs)
my r = [1..5] # Range-Literal materialisiert zum ArrayMehr dazu im Stdlib-Kapitel 13-module-und-builtins.md.
Slices und Range-Subscripts#
Bei Array- und String-Subscripts gibt es jetzt bewusst zwei verschiedene Formen fuer Ausschnitte:
value[a..b]bleibt ein inklusiver Range-Subscriptvalue[start:end]ist ein halboffener Slice mit exklusivemend
Fuer Arrays sehen die beiden Formen so aus:
my xs = [10, 20, 30, 40, 50]
println(xs[1..3])
println(xs[1:4])
println(xs[:3])
println(xs[3:])
println(xs[-3:-1])
# => [20, 30, 40]
# => [20, 30, 40]
# => [10, 20, 30]
# => [40, 50]
# => [30, 40]Wichtig ist die Semantik:
startist inklusivendist exklusiv- offene Grenzen defaulten zu Anfang bzw. Ende der Sequenz
- negative Indizes zaehlen vom Ende
- out-of-bounds wird geklemmt statt als Fehler behandelt
- wenn
end <= start, ist das Ergebnis leer
Bei Strings bleibt der Unterschied besonders sichtbar:
my text = "Hallo"
println(text[1..3])
println(text[1:4])
# => ["a", "l", "l"]
# => "all"text[1..3] liefert weiter ein Zeichen-Array, text[1:4] dagegen wieder
einen String.
Warum inklusiver Range, aber exklusiver Slice? Die beiden Formen bedienen zwei unterschiedliche Denkweisen:
- Ein Range
a..bbeschreibt eine Menge von Werten, die man wirklich meint — man nennt beide Endpunkte, die dazugehören.1..10heißt „1 bis einschließlich 10",'a'..'z'heißt „a bis z". Inklusiv ist hier die intuitive Lesart (wie in Mathe-Notation und Sprachen wie Ruby/Kotlin/Swift), und derselbe..-Range funktioniert auch außerhalb von Subscripts (for i <- 1..10,x in 1..10). - Ein Slice
start:endbeschreibt einen Ausschnitt zwischen Grenzen. Exklusivesendist die Konvention der meisten Sprachen (Python, Rust, Go, JavaScript) und hat handfeste Vorteile:end - startist direkt die Länge des Ausschnitts (xs[1:4]→ 3 Elemente).- Angrenzende Slices kacheln lückenlos und überlappungsfrei:
xs[0:k]undxs[k:]ergeben zusammen die ganze Sequenz — der Trennpunktkkommt genau einmal vor. xs[i:i]ist der leere Ausschnitt, undenddarflen(xs)sein (eins hinter dem letzten Index) ganz ohne Off-by-one-Rechnerei.
Kurz: .. schreibt man, wenn man an die Endwerte denkt, : wenn man an
Grenzen/Längen denkt.
Subscript ist einheitlich — auch fuer Grossbuchstaben-Namen#
name[...] ist immer ein Subscript, unabhaengig von der Gross-/
Kleinschreibung des Namens. Ein Tagged Array wird mit dem %-Praefix geschrieben
(%Tag[...], siehe Tagged Arrays), sodass es keine
Kollision zwischen Indexzugriff und Konstruktion gibt:
const GRID = [[1, 2], [3, 4]]
println(GRID[0]) # [1, 2] Index einer Konstante — keine Sonderregel
println(GRID[1..1]) # [[3, 4]] Slice/Range
println(%Ok[1, 2]) # %Ok[1, 2] Tagged Array (Konstruktion mit %)Es gibt zusaetzlich die aequivalente Punkt-Form x.[i] sowie die nil-sichere
Variante x?.[i] (undef statt Fehler bei undef-Empfaenger); beide unterstuetzen
Index, Slice, Range und Multi-Index wie x[i]:
maybe_grid?.[1..2] # Slice, oder undef falls maybe_grid == undefZeichen-Ranges und Membership#
Neben numerischen Ranges gibt es auch Zeichen-Ranges auf Basis von Einzelzeichen-Strings:
'a'..'d'
'a'..='e'
'a'..<'d'Die nackte ..-Form ist dabei die kurze inklusive Variante. Das ergibt normale
Folgen von Ein-Zeichen-Strings:
println('a'..'c') # ["a", "b", "c"]
println('a'..='c') # ["a", "b", "c"]
println('a'..<'d') # ["a", "b", "c"]
'x'..='z' |> take(2) |> &println
# => ["x", "y"]Ein Zeichen-Range entsteht, sobald beide Endpunkte Strings der Länge 1 sind —
egal ob als 'a', direktes Unicode-Zeichen 'α' oder Codepoint "\u{3b1}".
Ein längerer String als Endpunkt ('ab'..'z') ist ein Laufzeitfehler.
Ranges kannst du jetzt auch direkt mit in als Membership-Test verwenden:
println(3 in 1..10) # => true
println(23 in 1..10) # => false
println(4 in range(0, 10, 2)) # => true
println('b' in 'a'..'d') # => true
println('z' in 'a'..'d') # => falseDabei gilt:
- bei numerischen Ranges prueft
inStart, Ende und Step - bei Zeichen-Ranges prueft
inauf einzelne Zeichen im Bereich - bei offenen Ranges wie
5..*ist jeder passende Wert ab dem Start enthalten
a..b zaehlt immer aufwaerts. Ist b kleiner als a, ist die Range leer
(3..1 ergibt []) — so laeuft for i <- 0..(n - 1) bei n = 0 gar nicht
statt rueckwaerts. Abwaerts zaehlt range mit negativem Schritt; das Ende ist
dann die Untergrenze, und in, for und collect folgen derselben Richtung.
Ein Schritt 0 ist ein Fehler:
println(collect(3..1)) # => []
println(collect(range(10, 0, -3))) # => [10, 7, 4, 1]
println(4 in range(10, 0, -3)) # => true
println(5 in range(10, 0, -3)) # => falseKeyword Arrays#
Keyword Arrays im Ueberblick#
Keyword Arrays sind Arrays aus Key-Value-Paaren mit Atom-/Keyword-Keys. Sie wirken auf den ersten Blick ungewöhnlich, sind aber in Antra an mehreren Stellen praktisch:
- als kompakte Optionslisten
- als bare keyword args bei Aufrufen
- als Datenform, die sich leicht pattern-matchen lässt
[host: "localhost", port: 8080]
# ist dasselbe wie [[:host, "localhost"], [:port, 8080]]
println([host: "localhost", port: 8080])
# => [host: 'localhost', port: 8080]Angezeigt wird ein Keyword-Array so, wie man es schreibt (wie inspect in
Elixir): jedes Array, das nur aus Paaren [:name, wert] besteht, erscheint
als [name: wert, ...] — auch ein von Hand gebautes [[:a, 1]] oder ein
Map.entries(...)-Ergebnis, denn zur Laufzeit ist das derselbe Wert. Keys,
die man nicht als name: schreiben kann (:x-model), behalten die Paarform;
dd(...) zeigt immer die Paare.
Durchlaufen (for, map, Enum.*) liefert die Paare, nicht wie bei einer
Map die Werte — die Laufzeit kann ein Keyword-Array nicht von einer Liste von
Paaren unterscheiden. Schluessel und Wert per Destructuring:
Enum.map([a: 1, b: 2], |[k, v]| -> "${k}=${v}") # [':a=1', ':b=2']
for [k, v] <- [a: 1] { println(k, " ", v) }Merksatz fuer Enduser: Ein Keyword-Array ist einfach eine Liste von Optionen.
Du kannst die Paare entweder als host: "localhost" schreiben oder bewusst
mit sichtbarem Atom-Key als :host => "localhost".
Ein kleines Beispiel mit typischen Render-Optionen:
my opts = [:pretty => true, :indent => 4]
println(opts)
# => [pretty: true, indent: 4]Das ist besonders dann hilfreich, wenn du Optionen als Daten weiterreichen, speichern oder pattern-matchen willst.
Die Kurzform darf jetzt auch direkt mit ..spread gemischt werden:
[..opts, method: "POST"]
[method: "GET", ..opts, timeout: 30]
# => [..opts, [:method, "POST"]]
# => [[:method, "GET"], ..opts, [:timeout, 30]]Die allgemeine Schreibweise key => value desugart jetzt ueberall ausserhalb
echter Map-Literale %{...} zu [key, value].
:host => "localhost"
"port" => 8080
# => [:host, "localhost"]
# => ["port", 8080]In Funktions- und Lambda-Parametern sind Keyword Arrays als Pattern mit Defaults nuetzlich — inklusive der Kurzformen, bei denen du beim Aufruf die eckigen Klammern oder sogar das ganze leere Keyword Array weglassen kannst:
fn connect([host: = "localhost", port: = 80] as opts) ->
host <> ":" <> str(port)
connect() # => "localhost:80"
connect(host: "db", port: 5432)Die Regeln dazu (alle Keys mit Default; als letzter Parameter, vor einem
Callback oder vor ..rest) stehen ausfuehrlich in
Kapitel 08,
die Pattern-Seite in
Kapitel 07.
Keywords duerfen neben Buchstaben und Ziffern auch *, +, -, _, /, ?, !, ., $, %, =, <, > und & enthalten.
:*
:ready?
:user/id
:a+b
:<=::keyword hat in Antra aktuell keine Sonderbedeutung.
Wichtige Regeln fuer Keyword Arrays:
- Keys sind immer Keywords/Atoms
- doppelte Keys sind erlaubt; bei key-basiertem Lesen gewinnt typischerweise der letzte Wert
- Keyword Arrays bleiben regulaere Arrays aus Paaren:
my kw = [port: 80, port: 443]
kw[0] # => [:port, 80]
kw[0][0] # => :port
kw[0][1] # => 80Bare Keyword Args#
fetch("https://example.com", q: "search", limit: 10)
fetch("https://example.com", query:, limit:)
fetch(q: "search", limit: 10, 3)
fetch(q: "search", ..[10, 20])Das ist nur syntaktischer Zucker, intern bleibt es ein Keyword Array.
Wichtig dabei: Bare Keyword-Args muessen nicht am Ende der Argumentliste
stehen. Aufeinanderfolgende key: value-Argumente werden an ihrer Position zu
einem Keyword Array gebuendelt; das erste Nicht-Keyword-Argument beendet
es. Die dritte Zeile oben uebergibt also zwei Argumente — [q: "search", limit: 10] und 3 —, und in der vierten Zeile folgen nach dem Keyword Array
[q: "search"] die gespreizten Positionsargumente 10 und 20.
Das geht sogar mitten in der Argumentliste:
fn mid(a, opts, b) -> ...
mid(1, x: 5, 2) # a = 1, opts = [x: 5], b = 2Damit kann man im Prinzip das abbilden, was andere Sprachen als Named
Parameters eingebaut haben — nur ohne Sondermechanik: Ein Aufruf wie
mid(1, x: 5, 2) liest sich wie benannte Argumente, ist aber ein ganz
normaler Wert (ein Keyword Array) an einer ganz normalen Parameterposition,
den die Funktion per Pattern matchen kann. Viele Sprachen koennen das nicht:
Dort sind benannte Argumente fest an die Signatur gebunden, oder Options
landen zwingend als letzter Parameter (Ruby/Elixir: trailing Options-Hash
bzw. Keyword-Liste). Das trailing Keyword Array ist auch in Antra der
haeufigste Stil — aber eben nur Konvention, nicht Zwang. Als einziges hat es
Extra-Komfort: Wenn alle Keys Defaults haben, darf es beim Aufruf komplett
fehlen (siehe Kapitel 08, Abschnitt
Keyword-Array-Parameter).
Die explizite Form :key => value in einer Argumentliste ist dagegen jetzt ein
normales Pair-Argument und nicht mehr bare-keyword-args-Sugar.
Die implizite Kurzform ohne Wert funktioniert aber nur fuer bindbare Identifier. query: ist also Kurzform fuer query: query und ready?: fuer ready?: ready?, waehrend Keys wie x-model: oder user/id: ohne expliziten Wert nicht implizit gebunden werden.
Maps#
Maps und Zugriff#
Maps sind Schlüssel-Wert-Strukturen:
my user = %{name: "Ada", age: 36}
println(user.name)
println(user[:name])
# => "Ada"
# => "Ada"name: "Ada" legt einen Keyword-Key :name an (gleichwertig:
:name => "Ada"). String-Keys schreibt man mit =>: %{"name" => "Ada"};
%{"name": "Ada"} ist ein Parse-Fehler, weil der Doppelpunkt sonst immer einen
Keyword-Key bedeutet. Beide sind verschiedene Keys, wie in Elixir:
user["name"] findet :name nicht, user.name und user[:name] finden
"name" nicht, und eine Map kann beide nebeneinander enthalten. Dasselbe gilt
fuer in, Map.get, Zuweisungen und Map-Patterns (%{name: n} matcht nur
:name, %{"name" => n} nur "name").
my m = %{name: "Ada", "name" => "Bea"}
println([m.name, m[:name], m["name"]])
println(["name" in m, :name in m])
# => ['Ada', 'Ada', 'Bea']
# => [true, true]Daten von aussen (JSON, Formulare, Umgebung) kommen oft mit String-Keys an.
JSON.parse liefert Keys, die wie Bezeichner aussehen, als Keywords; fuer
dynamische Namen erzeugt keyword("name") den passenden Keyword-Key.
Fuer explizite oder dynamische Keys kannst du innerhalb von %{...} auch
=> verwenden:
my field = "display-name"
my user = %{ :role => "admin", field => "Ada" }
println(user[field])
# => "Ada"Wichtig ist die Unterscheidung zwischen statischem und dynamischem Zugriff:
user.name
user[field]
# => "Ada"
# => "Ada"Die stilistische Empfehlung ist jetzt bewusst einfach und JS-artig:
obj.namefuer statische Property-Namenobj[key]fuer dynamische Keys oder numerische Indizesobj?.namefuer sicheren statischen Property-Zugriffobj?.[key]fuer sicheren Klammer-Zugriffarr[i]undarr?[i]bleiben die normale Array-Form
Mit Klammern kannst du also sowohl Map-Keys als auch Array-Indizes lesen, ohne vorher die Zugriffsart wechseln zu muessen. Das ist besonders angenehm bei gemischten Datenstrukturen, in denen in einer Kette Maps und Arrays vorkommen koennen.
Beispiele:
my user = %{ profile: %{ city: "Berlin" } }
my key = "profile"
println(user.name)
println(user[key])
println(user?.[key]?.city)
my items = [10, 20, 30]
println(items[1])
println(items?[10] // "fehlt")
# => undef
# => %{ city: "Berlin" }
# => "Berlin"
# => 20
# => "fehlt"Noch zwei kurze Beispiele fuer den sicheren Array-Zugriff:
my values = ["a", "b", "c"]
my missing = undef
println(values?[1])
println(values?[10] // "n/a")
println(missing?[0])
# => "b"
# => "n/a"
# => undefWenn ein Feld fehlt, liefert normaler Property-Zugriff in vielen Faellen undef statt sofort zu crashen. Fuer nil-sichere Ketten gibt es zusaetzlich ?., ?.[...] und ?[...].
Fehlende Felder kombinieren sich gut mit dem Default-Operator //:
user.missing # => undef
user.missing // "?" # => "?"Deep-Set#
Mit := kannst du tief verschachtelte Strukturen aktualisieren:
my config = %{}
config.db.host := "localhost"
config.db.port := 5432
# => %{db: %{host: "localhost", port: 5432}}Dabei gilt aktuell:
- die Root-Bindung muss existieren
- die Root-Bindung muss mutierbar sein
- fehlende Zwischenstufen werden erzeugt
- fehlende Keys und fehlende Index-Pfade werden ebenfalls angelegt
Das ist praktisch, wenn du verschachtelte Maps oder Array/Map-Mischformen aufbauen willst.
Wichtig ist die Erzeugungsregel: := schaut nur auf den naechsten Schritt im Pfad.
- folgt als naechster Schritt ein Property- oder Key-Zugriff, wird eine Map erzeugt
- folgt als naechster Schritt ein Index-Zugriff, wird ein Array erzeugt
Dadurch funktionieren auch gemischte Faelle ohne Spezialsyntax:
my data = %{}
data.users[0].name := "Ada"
data.users[0].tags[0] := :admin
data.settings[:theme] := "light"
# => %{users: [%{name: "Ada", tags: [:admin]}], settings: %{theme: "light"}}Nach diesen Schreibzugriffen wurden die fehlenden Knoten unterwegs automatisch aufgebaut: users als Array, users[0] als Map, tags als Array und settings als Map.
Wichtig ist die Trennung der Operatoren:
=ist der strikte Write-Operator: der Zielpfad muss schon existieren:=ist der erzeugende Write-Operator: fehlende Zwischenknoten duerfen angelegt werden- beide Formen brauchen weiterhin eine vorhandene und mutierbare Root-Bindung
my config = %{}
config.db.port = 5432 # Fehler, weil db/port noch nicht da sind
config.db.port := 5432 # ok, legt den Pfad an
config.db.port = 6000 # ok, der Pfad existiert jetztMap-Update ohne Mutation#
Neben in-place Mutation gibt es auch funktionale Update-Formen:
my base = %{name: "Ada", age: 36}
my updated = %{base | age: 37}Das erzeugt eine neue Map. base bleibt unverändert. Links vom | darf jeder
Ausdruck stehen, der eine Map liefert — %{state.player | x: 0} oder
%{load(id) | age: 37} brauchen keinen Zwischen-Namen.
Für Leute aus FP-Sprachen ist das ein vertrautes Muster. Für Leute aus JS kann man es sich wie ein fokussierteres Objekt-Update vorstellen.
Das Update ersetzt auf oberster Ebene: Steht rechts vom | ein Key mit einem
verschachtelten Wert, wird der alte Unterbaum komplett ersetzt, nicht gemerged.
Fuer tiefes Zusammenfuehren gibt es den Spread-Abschnitt unten.
Spread und Deep Merge#
Spread und Deep Merge:
my ext = %{ ..base, z: 3 }
my merged = %{ ..base, ..ext }
my deep = %{ ...nested1, ...nested2 }Dabei ist der Unterschied bewusst klein, aber wichtig:
..mapist ein flacher Spread beziehungsweise Merge auf der obersten Ebene...mapist ein tiefer Merge fuer verschachtelte Map-Strukturen
Praktisch heisst das: Bei .. wird ein verschachtelter Unterbaum komplett
durch den spaeteren Wert ersetzt, waehrend ... versucht, verschachtelte Maps
rekursiv zusammenzufuehren.
Spaetere Einträge gewinnen, egal ob sie aus Spreads oder expliziten Keys kommen.
Eine getaggte Map darf ebenfalls Quelle sein; gespreadet werden ihre Daten. Den Tag bestimmt dabei die aeussere Literal-Form:
my a = %Point{ x: 1 }
%Point{ ..a, y: 2 } # %Point{ :x => 1, :y => 2 }
%{ ..a, y: 2 } # %{ :x => 1, :y => 2 } - ungetaggtGenau hier unterscheiden sich die beiden Formen: %{base | …} aktualisiert
einen Wert und behaelt darum dessen Tag, auch in einem ungetaggten Literal —
%{self | age: 31} in einer Package-Methode bleibt also ein %Person{…}. Ein
Spread dagegen baut ein neues Literal, dessen Form entscheidet.
Was nicht geht: | nimmt genau eine Basis (%{a | b} ist ein Parse-Fehler),
Spread beliebig viele. Und als Spread-Quelle muss eine Map oder getaggte Map
stehen, sonst:
Error: spread in a map literal requires a map or tagged map; got Array.
Sets und Tagged Sets#
Sets#
Sets werden mit %<...> geschrieben:
my ids = %<1, 2, 2, 3>Sie sind gut für Mitgliedschaftsprüfungen mit in.
Zur Einordnung:
x in setprueft Set-Membershipx in arrayprueft Equality-Membershipkey in mapprueft Map-Keysx in 1..10prueft Range-Membership
Tagged Sets#
Tagged Sets geben einem normalen Set zusaetzlich einen fachlichen Tag:
my seen = %Seen<1, 2, 3>
my roles = %Roles<:admin, :editor>Das ist die Set-Schwesternform zu:
- Tagged Arrays wie
%Ok[42] - Tagged Maps wie
%User{name: "Ada"}
Praktisch ist das dann, wenn die Werte semantisch klarer werden sollen, ohne die Set-Eigenschaften zu verlieren.
Zum Beispiel koennen zwei Sets denselben Inhalt haben, aber etwas anderes meinen:
my seen = %Seen<1, 2, 3>
my selected = %Selected<1, 2, 3>Beide sind Sets, aber der Tag traegt die zusaetzliche Bedeutung.
Aktuell gilt fuer die erste Stufe:
%Tag<...>dedupliziert genauso wie%<...>- Tagged Sets bleiben normale Set-Werte mit zusaetzlichem Tag
- Gleichheit beruecksichtigt sowohl Elemente als auch den Tag
tagged_set?(...)erkennt getaggte Sets- linksgerichtete Set-Operationen wie
Set.union(...),Set.intersect(...),Set.diff(...)unda \\ bbehalten den linken Tag bei
import Antra::Set as Set
my base = %Seen<1, 2>
println(Set.union(base, %<2, 3>)) # => %Seen<1, 2, 3>
println(base \\ %<2>) # => %Seen<1>Auch select (...) yield ... kann Tagged Sets als Seed-Ziel verwenden:
my out = select (
x <- [2, 3, 3, 4]
into: %Seen<1, 2>
) yield x
println(out) # => %Seen<1, 2, 3, 4>Set-Patterns (exakt, offen, mit Rest und mit Tag) sind in Kapitel 07 beschrieben — kurz:
match ids {
%<1, 2> -> "genau 1 und 2"
%Seen<1, 2> -> "genau dieses tagged set"
%<1, 2, ..rest> -> rest
_ -> "anderes"
}Tagged Values#
Das
%-Praefix ist der Tag-Marker. Tagged-Value-Literale schreibt man als%Tag[...](Array),%Tag{...}(Map) und%Tag<...>(Set); Tagged Strings alstag"...". Der Tag darf gross oder klein geschrieben sein —%Ok[1, 2],%ok[1, 2]und%point{x: 1}sind alle gueltige Literale.Das
%ist noetig, weilname[...]ohne Praefix schon eine andere Bedeutung hat: Es liest einen bestehenden Wertnameund indiziert hinein. Erst das%macht daraus ein neues Tagged-Array-Literal, dessen Tag der Name selbst ist:val xs = [10, 20, 30] xs[1] # => 20 (Subscript: greift auf Element 1 von xs zu) %xs[1] # => %xs[1] (Tagged Array mit Tag "xs" und dem Element 1)Dieselbe Regel bei Grossschreibung:
Config[0]schlaegtConfigals Variable oder Paket nach (und indiziert), waehrend%Config[0]das Tagged-Array-Literal mit TagConfigist. Fuer Maps ({...}) und Sets (<...>) gilt das analog.%rgb[255, 128, 0] # Tagged Array mit Tag "rgb" %point{x: 1} # Tagged Map %seen<1, 2> # Tagged Set sql"SELECT 1" # Tagged String (== %sql["SELECT 1"]) 10sec # Tagged Scalar (Zahl + Tag, == %sec[10])Tags sind case-sensitiv:
%okund%Oksind verschiedene Tags — genau wieokundOkverschiedene Bezeichner sind. Das ist kein Sonderfall der Schreibweise, sondern die normale Namensgleichheit. Ein%ok[1]matcht also nicht ein%Ok[1], und umgekehrt.Technisch ist
%ok[...]voellig gueltig — es gibt keine Einschraenkung. In der Praxis solltest du aber Tags vermeiden, die sich nur in der Gross-/ Kleinschreibung von den eingebauten unterscheiden (%okvs%Ok,%somevs%Some): Das fuehrt leicht zu Verwirrung — besonders als Rueckgabewert, wo Leser%okfaelschlich fuer das Result-%Okmit.^/.?halten. Nimm fuer eigene Daten lieber klar unterscheidbare Tags.Sonderfall Result/Option. Die Tags
%Ok/%Err(Result) und%Some/%None(Option) verhalten sich nur deshalb besonders (Operatoren.^Propagation,.?Soft-Unwrap), weil die Standardbibliothek dafuer Protokoll-Impls mitbringt (impl Propagate for %Ok[..]inlib/Antra/Propagate/*, analogUnwrap). Es ist also keine Magie an der Grossschreibung: jeder andere Tag — ob%User,%Pointoder%ok— ist ein schlichter Tagged Value, bis man selbst ein solches Protokoll ergaenzt. Konkret:%Ok[5].^funktioniert,%ok[5].^nicht. DeshalbOk/Err/Some/Noneals die kanonischen (grossgeschriebenen) Result/Option-Tags nutzen und eigene Daten mit eigenen Tags versehen.
Antra::Valueist fuer DYNAMISCHE Tags. Das Literal nimmt den Tag woertlich (%t[1]ist der Tag"t", nicht der Wert der Variablet). Soll der Tag erst zur Laufzeit aus einem String/einer Variable kommen, nimmt man die Konstruktoren:import Antra::Value as Value val kind = "ok" Value.tagged_array(kind, [1, 2]) # => %ok[1, 2] (Tag aus der Variable) Value.tagged_map("point", %{x: 1}) # => %point{ :x => 1 } Value.tagged_string("sql", "SELECT 1") # => %sql["SELECT 1"] Value.tagged("mytag", 42) # => %mytag[42]
Value.tag(v),Value.data(v)undValue.untag(v)lesen einen Tagged Value wieder aus.
Tagged Values sind Antras leichtgewichtige „Typen". Der Tag wirkt wie ein
nominaler Typname, und über Protokolle lässt sich Verhalten daran hängen: Ein
impl <Protokoll> for %Tag[..] gibt allen Werten mit
diesem Tag Operatoren, Konvertierungen oder Sonder-Semantik. Genau darauf bauen
die eingebauten Fälle auf — nichts davon ist im Kern hartverdrahtet:
%Ok[..]/%Err[..]und%Some[..]/%None[..]bekommen.^/.?überimpl Propagate/impl Unwrap(lib/Antra/Propagate/*,lib/Antra/Unwrap/*).- Durations wie
10min,5secrechnen überimpl Duration for %min[..]&co. (lib/Antra/Duration.ant) miteinander.
Dieselben Mittel stehen dir für eigene Tags offen: implementierst du ein Protokoll
für %rgb[..], verhalten sich alle %rgb[...]-Werte entsprechend. Ein Tagged
Value ohne solche Impls bleibt einfach ein benannter Datencontainer.
Tagged Maps#
Tagged Maps sind Maps mit zusätzlicher Forminformation:
my user = %User{name: "Ada", age: 36}Das ist besonders nützlich, wenn unterschiedliche Datenformen ähnlich aussehen, aber semantisch etwas anderes bedeuten.
Beispiel:
my billing = %BillingAddress{city: "Berlin"}
my shipping = %ShippingAddress{city: "Berlin"}Beide haben dieselben Felder, aber der Tag erlaubt später ein präzises Matching.
Tagged Arrays#
Tagged Arrays sind die positionsorientierte Schwester dazu:
my ok = %Ok[42]
my err = %Err["kaputt"]Das eignet sich gut für Result-Typen, AST-ähnliche Werte oder kleine algebraische Datentypen.
Tagged Scalars#
Tagged Scalars sind die kompakte Schreibweise für einen skalaren Wert mit Tag:
10sec
3.5kg
10_000msPraktisch ist das vor allem dann, wenn nackte Zahlen am Call-Site zu unklar
wären. 10sec ist lesbarer als ein bloßes 10, weil die Einheit direkt im
Wert steckt.
Wenn dir die Zahlensyntax mit Trennern noch neu ist, steht die Grundlage dazu in 04-grundlagen.md im Abschnitt zu Zahlentrennern.
Aktuell gilt:
- Tagged Scalars werden intern wie Tagged Arrays mit genau einem Element behandelt
- Pattern Matching funktioniert direkt mit lowercase-Tags, im Pattern mit
%-Präfix (%ms[v]) - der Tag folgt dem Zahlteil ohne Leerzeichen
fn to_ms(%ms[v]) -> v
fn to_ms(%sec[v]) -> v * 1000
println(to_ms(250ms)) # => 250
println(to_ms(3sec)) # => 3000Zifferntrenner funktionieren auch im Zahlteil von Tagged Scalars:
10_000sec
1_000.25kgBewusst nicht unterstützt ist aktuell eine allgemeine Schreibweise wie
sec[10], weil name[index] bereits normaler Subscript-Zugriff ist.
Ein vollständiges, lauffähiges Beispiel findest du in ../examples/tagged_scalars.ant.
Tagged Strings#
Dieselbe Idee funktioniert auch für Strings: Der Tag steht direkt vor dem String-Literal, ohne Leerzeichen.
html"<b>ok</b>"
sql"SELECT 1"Aktuell gilt für die erste Ausbaustufe:
- Tagged Strings werden intern wie Tagged Arrays mit genau einem Element behandelt
%html[s]oder%sql[s]funktionieren direkt im Pattern Matching (Pattern mit%-Präfix)- Tagged Strings gibt es in allen drei String-Formen:
tag"..."(Interpolation + Escapes),tag'...'(roh — keine Interpolation, keine Escapes) undtag`...`(Template — Interpolation aktiv, aber Backslashes bleiben literal). Für Regexes istregex`...`bevorzugt, damit\d,\wusw. literal bleiben
fn unwrap_html(%html[s]) -> s
println(unwrap_html(html"<div>ok</div>"))
match sql"SELECT 1" {
%sql[s] -> println(s)
}Ein kleines, lauffaehiges Beispiel dazu findest du in ../examples/tagged_strings.ant.
Noch nicht Teil dieser ersten Stufe sind mehrzeilige Fenced-Block-Varianten wie
fenced html- oder sql-Blöcke und auch kein spezielles Laufzeitverhalten pro
Tag.
Orientierung#
Wann nehme ich was?#
Eine grobe Heuristik:
- Array: wenn Reihenfolge wichtig ist
- Map: wenn benannte Felder wichtig sind
- Keyword Array: wenn du Optionen oder call-nahe Schlüssel-Wert-Paare modellierst
- Tagged Map: wenn benannte Felder plus Typ/Tag wichtig sind
- Tagged Array: wenn Form und Position wichtiger sind als Feldnamen
- Tagged Scalar: wenn ein einzelner skalarer Wert fachlich ein klares Tag tragen soll
- Tagged String: wenn ein String fachlich einen klaren Kontext oder Kanal tragen soll
Kleines Vergleichsbeispiel#
my person = %{name: "Ada", age: 36}
my user = %User{name: "Ada", age: 36}
my ok = %Ok[person]personist nur eine normale Mapuserist eine Map mit TagUserokist ein Tagged Array mit TagOkund der person
