Sprache
Grundlagen
Dieses Kapitel sammelt die Basissyntax: Literale, Variablen, Operatoren und rationale Zahlen.
Literale und Typen#
Antra kennt die üblichen Grundwerte:
- Ganzzahlen wie
42oder-7, beliebig groß und immer exakt - Dezimalzahlen auch mit Trennern wie
10_000oder1_000.25 - Hex-, Oktal- und Binärliterale wie
0xFF,0xDEAD_BEEF,0o755oder0b1010_0101(Oktal z. B. für Dateirechte:FS.chmod(path, 0o755)) - Zahlen mit Exponent wie
1e300,6.02e23oder1.5e-3 - Strings mit doppelten oder einfachen Anführungszeichen (ein einzelnes Zeichen
ist einfach ein String der Länge 1, z.B.
'a') true,falseundundef- Arrays, Maps, Sets und Tagged Values
42
10_000
1_000.25
0xFF
0xDEAD_BEEF
0o755
0b1010
0b1010_0101
6.02e23
1.5e-3
"hello"
'raw string'
`a`
true
false
undef
[1, 2, 3]
%{name: "Ada"}
%<1, 2, 3>
%Ok[42]
%User{name: "Ada"}Zahlentrenner mit _#
Große Zahlen lassen sich mit _ besser lesbar schreiben:
10_000
1_000_000
1_000.25
0xDEAD_BEEF
0b1010_0101Wichtig:
_ist nur als Trenner zwischen Ziffern erlaubt1__000ist ein Parse-Fehler1_ist ein Parse-Fehler
Die Trenner verändern nur die Schreibweise, nicht den Wert.
Große Ganzzahlen#
Ganzzahlen haben keine Obergrenze. Passt ein Ergebnis nicht mehr in 64 Bit, rechnet Antra exakt weiter:
println(9223372036854775807 + 1) # 9223372036854775808
println(2 ** 100) # 1267650600228229401496703205376Wie Int, Rat und Float zusammenspielen, beschreibt 05-zahlen.md ausführlich.
Zahlen mit Exponent#
e oder E mit einem ganzzahligen Exponenten schreibt sehr große und sehr
kleine Zahlen: 1e300, 6.02e23, 1.5e-3, 2E+10. Wie Dezimalzahlen sind
sie exakt: Ein ganzer Wert ist eine Ganzzahl (1e3 ist 1000, 1e300
eine 1 mit 300 Nullen), jeder andere ein Bruch (1.5e-3 ist 3/2000, und
1.5e-3 * 2000 ist genau 3). Ein Literal darf höchstens 10 000 Stellen
ergeben; 1e20000 ist ein Fehler, schreib dafür 10 ** 20000.
Folgt nach dem Exponenten noch ein Buchstabe, ist es keine Zahl, sondern ein
Tagged Scalar: 5em ist %em[5] (CSS-Einheit), ebenso 1e ohne Ziffern.
Wenn du von nackten Zahlen zu getaggten Werten wie 10sec oder 10_000ms
wechseln willst, lies in 06-collections.md den Abschnitt zu
Tagged Scalars weiter.
Die drei String-Typen#
Antra kennt drei Anführungszeichen-Arten, die sich in genau zwei Fragen
unterscheiden: interpoliert der String? und werden Escape-Sequenzen wie
\n verarbeitet?
| Form | Interpolation | Escapes (\n, \u{...}) |
|---|---|---|
"..." (doppelt) | ✓ | ✓ |
'...' (einfach) | ✗ | ✗ (roh) |
`...` (Backtick) | ✓ | ✗ (roh) |
Doppelte Strings#
Doppelte Anführungszeichen erlauben Interpolation und Escape-Sequenzen:
my name = "Ada"
println("Hallo $name")
println("Zeile 1\nZeile 2")Einfache Strings#
Einfache Anführungszeichen sind raw Strings. Das ist praktisch, wenn du möglichst wenig Sonderbehandlung willst:
'raw string'
println('Hallo $name')
println('Zeile 1\nZeile 2')In einfachen Strings bleiben $name und \n also wörtlich im Text.
Backtick-Strings (Template)#
Backticks sind die Mischform: Interpolation ja, Escapes nein. $name und
${expr} werden ausgewertet, aber Backslashes bleiben wörtlich im Text — kein
\n, kein \u{...}.
my name = "Ada"
println(`Hallo $name`) # Hallo Ada
println(`Summe: ${1 + 2}`) # Summe: 3
println(`Pfad C:\neu\tab`) # C:\neu\tab (Backslashes bleiben literal)Die Hauptmotivation sind Regexes: dort willst du Backslashes (\d, \w) und
$-Anker unverändert lassen, aber trotzdem Werte einsetzen. Ein $, dem kein
Bezeichner oder { folgt, bleibt dabei literal — der Regex-Anker $ überlebt:
my n = 4
my code = regex`\d{$n}$` # matcht genau 4 Ziffern am ZeilenendeGetaggte Strings gibt es in allen drei Formen: regex"..." (escaped),
regex'...' (raw) und regex`...` (template). Der Tag vor dem String
bestimmt, wie der Inhalt interpretiert wird — die Quote-Art nur, ob interpoliert
und ob Escapes verarbeitet werden.
Einzelne Zeichen#
Antra hat keinen eigenen Zeichen-Typ und keine Backtick-Literale. Ein einzelnes Zeichen ist einfach ein String der Länge 1:
'a'
'Z'
"\u{3b1}" # α per Unicode-Codepoint (nur in doppelten Strings)Wichtig:
- die Hauptanwendung ist die Zeichen-Range, zum Beispiel
'a'..'z' - ein Range-Endpunkt muss ein String der Länge 1 sein —
'ab'..'z'ist ein Laufzeitfehler - Unicode-Zeichen kannst du direkt schreiben (
'α'..'ω') oder in doppelten Strings per Codepoint mit\u{HHHH}
Variablen mit my, val und const#
my count = 1
count = count + 1
val user = %{name: "Ada"}
user.name = "Ada Lovelace"
const answer = 42Merke dir die Grundregel als 3-Stufen-Modell:
my: Bindung ist rebindbar.val: Bindung ist nicht rebindbar, aber der Wert bleibt mutierbar.const: Bindung ist nicht rebindbar und Composite-Werte werden transitiv eingefroren.
Kurzer Vergleich:
| Deklaration | Rebinding (x = ...) | In-place Mutation (x.a := ..., x[i] = ...) |
|---|---|---|
my | erlaubt | erlaubt |
val | verboten | erlaubt |
const | verboten | verboten |
Beispiel fuer val:
val cfg = %{ db: %{ port: 5432 } }
cfg.db.port = 6000 # erlaubt
cfg = %{} # FehlerAntra ist lexikalisch gescoped. Ein Name gilt also dort, wo er gebunden wurde, plus in inneren Blöcken. Im gleichen Scope darf ein Name nur einmal deklariert werden. In Kind-Scopes darf derselbe Name erneut gebunden werden; der innere Name shadowt dann den aeusseren nur lokal.
Weil das in der Praxis leicht missverstanden wird, hier die konkretere Faustregel:
do { ... }erzeugt einen neuen lokalen Scope.- Funktions- und Lambda-Bodies erzeugen einen neuen lokalen Scope.
while (...) { ... }erzeugt pro Iteration frische lokale Bindungen.- Zusaetzlich sind jetzt zwei lesenahe Kurzformen moeglich:
while name <- expr { ... }fuer den engenundef-Stop-Fall undwhile pattern <- expr { ... }fuer Pattern-getriebene Schleifen wiewhile %Some[line] <- File.read_line(fh).! { ... }. for x <- seq { ... }ebenfalls, weilforpro Durchlauf frische lokale Bindungen anlegt, egal ob die Schleife intern ueber den direktenfor_statement-Pfad oder denforeach(...)-Fallback laeuft.- Ein
if (...) { ... } else { ... }-Block ist dagegen in erster Linie ein Kontrollfluss-Block, nicht automatisch ein neuer allgemeiner Shadowing-Container.
Das heisst: Ein inneres const x in einem do-Block ist erlaubt, eine zweite Deklaration danach im aeusseren Scope aber nicht.
const x = 10
do {
const x = 20
println(x) # 20
}
println(x) # 10
const x = 30 # Fehler: wieder derselbe aeussere ScopeDasselbe gilt auch fuer Imports: import Antra::String as String oder import Antra::Array.* fuehren Namen im aktuellen Scope ein. Eine spaetere Deklaration mit demselben Namen im selben Scope ist deshalb ebenfalls ein Redeclaration-Fehler.
Bei Schleifen ist wichtig, die zwei Ebenen nicht zu vermischen:
- Innerhalb einer
while- oderfor-Iteration duerfen lokalemy-Bindings des Schleifenbodys pro Durchlauf neu deklariert werden. - Namen aus dem aeusseren Scope bleiben dabei sichtbar und koennen, falls sie mit
mygebunden sind, per=aktualisiert werden.
my total = 0
while (total < 3) {
my label = "step-" <> str(total)
println(label)
total = total + 1
}Namen mit ? und !#
Ein Name darf auf ? oder ! enden - Funktionen, my/val/const,
Parameter und Lambda-Parameter. Üblich ist ? für Wahrheitswerte und
Prüfungen, ! für Funktionen, die bei einem Fehler werfen:
fn even?(n) -> n % 2 == 0
val ready? = len([1, 2]) > 1
println(filter([1, 2, 3, 4], even?)) # [2, 4] - even? ohne Klammern ist der Funktionswert
println(map([true, false], |on?| -> !on?))x?.name und x?[i] bleiben die sicheren Zugriffe; dort gehört das ? nicht
zum Namen. defined? und undef? sind reservierte Operatoren und keine
Namen. Das frühere Postfix-? gibt es nicht mehr: steht x?, wo nur x
existiert, nennt die Fehlermeldung x.^ (Fehler weiterreichen) und x.?
(weich auspacken).
Warum sagt die Doku "Binding" statt "Variable"?#
Weil in funktional geprägten Sprachen oft betont wird, dass ein Name an einen Wert gebunden wird. Für den Alltag kannst du aber ruhig weiter "Variable" denken.
Relevant ist eher der Unterschied:
- eine Bindung kann unveränderlich sein
- ein Wert kann trotzdem zusammengesetzt sein
- bei zusammengesetzten Werten kann man bewusst in-place mutieren, aber nur wenn die Root-Bindung mutierbar ist
Wichtig fuer zusammengesetzte Werte:
const user = %{...}bedeutet auch:user.name = ...unduser.profile.city := ...sind nicht erlaubtconst xs = [...]bedeutet auch:xs[0] := ...undxs[1] := ...sind nicht erlaubtconst s = %<...>bedeutet auch:s \\= otherundSet.add(s, x)sind nicht erlaubtconstfriert bei Arrays, Maps und Sets den erreichbaren Wertgraphen transitiv ein: wennconst outer = [inner]gilt, ist danach auchinner[0] = ...nicht mehr erlaubt- ein
const-Composite darf ausserdem nicht an einenmut-Parameter uebergeben werden - ein
val-Composite darf dagegen anmut-Parameter uebergeben werden, weilvalnur Rebinding sperrt, aber nicht einfriert
Operatoren#
Arithmetik#
1 + 2
10 - 3
4 * 5
20 / 4
2 ** 8
1 <=> 2
2 <=> 2
"file2" <~> "file10"
"ab" <=> "aa"Kurze Compound-Assignments gibt es ebenfalls:
x += 1
x &= 3
x |= 4
x <<= 1
x >>= 1
remaining \\= blocked
s <>= "!"
out ++= [value]Bit-Operatoren#
&, |, ^ (XOR, nicht Potenz!), <<, >> und das präfixe ~ (bitweises
NOT) rechnen auf vorzeichenbehafteten 64-Bit-Ganzzahlen, wie in C oder
Python. Brüche und Floats werden vorher auf ganze Zahlen abgeschnitten; eine
ganze Zahl außerhalb von 64 Bit ist ein Fehler:
println(12 & 10) # 8
println(12 | 3) # 15
println(12 ^ 10) # 6
println(1 << 4) # 16
println(-6 >> 1) # -3 (arithmetischer Shift)
println(~5) # -6 (~n ist -n - 1)
println(0b1111 & ~0b10) # 13 (Bit 1 löschen)~ bindet wie das unäre Minus (~1 + 1 ist (~1) + 1).
String-Konkatenation#
Strings werden mit <> verbunden:
"Hallo" <> " " <> "Welt"Das ist bewusst nicht +, damit Zahlen- und String-Operationen klar getrennt bleiben.
Gleichheit#
Antra unterscheidet mehrere Arten von Gleichheit:
==für tiefen Wertvergleich===für typstrikten tiefen Vergleich=:=für Referenzidentität
Für Einsteiger ist == meistens der richtige Start.
Logische Operatoren &&, ||, !#
&& und || verhalten sich wie in Perl (oder JavaScript/Python): sie
kurzschliessen und geben den Operanden selbst zurueck, nicht true/false.
println(0 || "fallback") # "fallback" - erster wahrer Operand
println("a" && "b") # "b" - letzter, wenn alle wahr
println(0 && teuer()) # 0 - teuer() wird nie ausgewertetDamit funktioniert der klassische Kurzschluss-Stil direkt:
val name = eingabe || "anonym" # Default, wenn eingabe falsy
do_something() && println("Erfolg") # println nur bei ErfolgEigene Wort-Operatoren and/or/not gibt es bewusst nicht: &&/||
binden bereits lockerer als Vergleiche (a == b && c == d braucht keine
Klammern), und die Statement-Kurzschluss-Faelle deckt Antra ohne Perls
Praezedenz-Fallen ab.
Was gilt als wahr?#
if, &&, || und ! benutzen dieselbe Wahrheits-Definition. Falsy sind
die "leeren" Werte - inklusive leerer Collections (Python-Stil):
| falsy | truthy |
|---|---|
false | true |
0 | alle anderen Zahlen (auch negative) |
"" (leerer String) | jeder nicht-leere String |
undef | nicht-leere Collection |
[], %{}, %<> (leere Collections) | jedes Objekt / jede Referenz |
Damit liest sich if items { … } als "wenn es Items gibt", und
val name = eingabe || "anonym" greift auch bei einer leeren Liste.
Zwei Details fuer Perl-Umsteiger:
- Der String
"0"ist in Antra wahr. In Perl ist er falsch - eine bekannte Stolperfalle ("0"aus einer Eingabe wird ploetzlich falsy). Antra macht es einheitlich: jeder nicht-leere String ist wahr, egal was drinsteht. - Leere Collections sind falsy, nicht wie in Perl truthy (dort ist eine
Array-Referenz immer wahr).
[],%{}und der leere Set%<>verhalten sich wie in Python. Willst du "existiert vs. leer" trennen, pruefe explizit gegenundef(defined? x) - denn ein leeres, aber vorhandenes[]ist falsy.
Tagged-Varianten (%Ok[…], %Err[…]) sind keine Collections und bleiben
truthy, auch ohne Nutzlast - %Ok[] ist wahr.
Regex-Match mit =~#
=~ prueft, ob ein String auf ein Pattern passt, und liefert immer true oder false.
In normalen Ausdruecken bleibt das auch so. Zusaetzlich gibt es jetzt fuer den kleinen lokalen Erfolgsfall branch-lokale Capture-Bindings im ausgefuehrten Match-Branch:
if "2026-05-19" =~ regex"(?<year>\\d{4})-(\\d{2})-(\\d{2})" {
println(year)
println(_1)
println(_2)
println(_3)
}Dabei gilt:
- benannte Gruppen wie
yearsind nur im erfolgreichenthen-Branch sichtbar - nummerierte Gruppen werden als
_1,_2,_3, ... gebunden - ausserhalb des Branches existieren diese Namen nicht
- wenn das Pattern keine Gruppen enthaelt, gibt es auch keine zusaetzlichen Bindings
- optionale, nicht belegte Gruppen werden trotzdem als
_n = undefgebunden - sichtbare Namen wie
yearoder_1duerfen dabei nicht still ueberschrieben werden
Mit unless sitzt derselbe Match-Fall entsprechend im else-Branch:
unless "world" =~ regex"(?<prefix>^wor)(ld)" {
println("fallback")
} else {
println(prefix)
println(_1)
println(_2)
}println("hello" =~ regex"^hel")
println("world" =~ regex"^hel")Rechts darf entweder ein Regex-Literal oder ein bereits kompiliertes Regex stehen:
import Antra::Regex as Regex
val rx = Regex.compile("^item-[0-9]+$").!
println("item-42" =~ rx)Die ueblichen String-Operationen binden staerker als =~. Du kannst also vorher zusammensetzen und danach matchen:
println("file" <> str(2) =~ regex"^file2$")Wenn du statt eines booleschen Ergebnisses lieber optional die Match-Daten willst, gibt es ~?:
match "2026-05-19" ~? regex`(?<year>\d{4})-(\d{2})-(\d{2})` {
%Some[parts] -> {
println(parts[0]) # ganzer Treffer -> "2026-05-19"
println(parts.year) # benannte Gruppe -> .name
println(parts[2]) # unbenannte Gruppe -> [nr]
}
%None[] -> println("kein Treffer")
}~? liefert bei einem Treffer %Some[...], sonst %None[]. Die Form haengt
davon ab, ob das Muster Capture-Gruppen hat:
- Ohne Gruppen ->
%Some[string]: einfach der getroffene Text (z.B.%Some["hel"]). - Mit Gruppen ->
%Some[map]: Key0ist der ganze Treffer (wie Perl$&), benannte Gruppen(?<name>...)liegen unter ihrem Namen (parts.name), unbenannte unter ihrer Gruppennummer (parts[2]).
Tipp: Für Regex-Muster ist der Backtick-String regex`...` am angenehmsten —
Backslashes bleiben literal, du sparst dir das doppelte Escapen von \d, \w
usw. (in regex"..." müsstest du \\d schreiben).
Faustregel:
if text =~ rx { ... }fuer kleine lokale Branches mit Capture-Bindingsunless text =~ rx { ... } else { ... }wenn der Nicht-Match-Fall der Hauptpfad isttext =~ rxfuer reine Bool-Pruefungentext ~? rxoderRegex.match(...)wenn du Match-Daten als normalen Wert weiterreichen willst
defined?#
Mit dem benannten Praefix-Operator defined? pruefst du, ob ein Wert nicht
undef ist:
if defined? user.name then println("hat Namen")Das ist etwas anderes als allgemeine Truthiness. false und 0 sind nicht undef.
Rationale Zahlen#
Ein besonderes Sprachmerkmal von Antra ist exakte Rational-Arithmetik.
1/3
1/2 + 1/4Die Sprache speichert solche Werte nicht als ungenaue Floating-Point-Zahlen, sondern als rationale Werte. Das ist vor allem bei Rechenlogik angenehmer, weil sich typische Rundungsfehler vermeiden lassen. Auch Dezimalzahlen wie 0.1 sind solche Brüche, deshalb ist 0.1 + 0.2 == 0.3 wahr.
Wenn du aus JavaScript kommst, ist das ein echter Unterschied. Mehr dazu, auch
wann float(...) die bessere Wahl ist, steht in 05-zahlen.md.
Konvertierungen#
Es gibt einige eingebaute Konvertierungsfunktionen:
str(42)
num("12")
int(3.9)
float(1/3)
rat("1/3")Praktische Faustregel:
str(...)für Darstellungnum(...)wenn ein String als Zahl gelesen werden sollint(...)wenn du eine Ganzzahl brauchstfloat(...)wenn exakte Bruchrechnung enden soll, z.B. vorv * 0.95in jedem Frame (Dezimal-Literale sind exakte Brueche, deren Nenner bei langen Rechenketten wachsen)rat(...)wenn du bewusst einen exakten Rational-Wert haben willst
Ein kleines Gesamtbeispiel#
my subtotal = 19.99
my tax = 1/5
my total = subtotal + subtotal * tax
println("Gesamt: " <> str(total))Hier sieht man schon gut, wie Antra gedacht ist:
- Werte werden direkt kombiniert
- Ausdrücke bleiben klein und lesbar
- Konvertierung in String passiert erst beim Ausgeben
Referenz: Vergleich, Membership und Ranges#
Für den Alltag reicht oft ==, aber die alte Referenz unterschied diese Fälle ausdrücklich:
==für tiefen Wertvergleich===für typstrikten tiefen Vergleich=:=für Referenzidentität bei zusammengesetzten Werten
5 == 5.0 # true
5 === 5.0 # false
my a = [1, 2]
my b = [1, 2]
my c = a
a == b # true
a =:= b # false
a =:= c # true== wandelt nie um: Werte verschiedener Typen sind einfach ungleich
("1" == 1 ist false, :ok == "ok" ebenso). Nur die Ordnung wirft dann einen
Fehler ("1" < 1); umgewandelt wird explizit mit num(...) oder str(...).
Mit in prüfst du je nach Datentyp Mitgliedschaft oder Key-Existenz:
2 in %<1, 2, 3>
"b" in ["a", "b", "c"]
:name in %{name: "Alice"}Ranges sind ein eigener Datentyp und tauchen sowohl in Iteration als auch in Literalen auf:
1..=10
1..<10
1..*
0..100 by 5 # 0, 5, 10, ..., 100
10..0 by -3 # 10, 7, 4, 1: ein negativer Schritt zaehlt abwaertsby legt die Schrittweite fest (wie range(0, 100, 5)); es ist nur direkt
hinter einer Range auf derselben Zeile ein Schluesselwort, by bleibt als
Name frei.
