Sprache
Kontrollfluss, Fehlerbehandlung und Blöcke
Antra hat klassische Kontrollflussformen, aber mit einem Ausdrucks-freundlichen Stil: if, try und Comprehensions sind Ausdrücke, Pattern Matching sitzt direkt in den Kontrollflussformen, und use macht Callback-APIs linear lesbar.
Verzweigungen mit if und unless#
Block-Form und kompakte Form#
Es gibt zwei häufige Lesarten:
if (x > 0) {
println("positiv")
}oder ohne Klammern:
if x > 0 {
println("positiv")
}und
if x > 0 then println("positiv") else println("nicht positiv")Die kompakte Ausdrucksform braucht dabei immer then. Die nackte Form
if cond expr else expr ist nicht Teil der Syntax.
then und Block-Form: die Trennregel#
then und Block-Form schliessen sich gegenseitig aus: then leitet
genau einen Ausdruck ein (die Ternary-Lesart cond ? a : b), ein
{ ... }-Branch schreibt sich ohne then. Die Mischform if cond then { ... } ist ueberall ein Parse-Fehler — auch in Wert-Position:
if cond then { work() } # Parse-Fehler
val z = if cond then { "x" } else 2 # Parse-Fehler
val z = if cond { "x" } else { 2 } # ok: Block-Form ohne then
val z = if cond then "x" else 2 # ok: kompakte FormEin einzelnes Statement als Body#
Ein kompakter Body muss kein Ausdruck sein. Wo ein { ... }-Block als Body
erlaubt ist, darf auch ein einzelnes Statement ohne Klammern stehen —
eine Zuweisung, return, last, next oder redo:
if (done) then running = false
while (i < 3) i += 1
for (n <- xs) do sum = sum + nDas gilt genauso fuer Match-Arme (siehe Kapitel 7) und ist bei Funktionskoerpern seit jeher so:
fn set(v) -> counter = vSobald mehrere Anweisungen zusammengehoeren, brauchst du weiterhin einen Block:
if (done) {
running = false
log("stopped")
}Eine Zuweisung liefert dabei den zugewiesenen Wert, der Body bleibt also auch als Ausdruck brauchbar.
Die Grenze: Bedingungen bleiben ausdrucksfrei#
Eine Zuweisung ist damit trotzdem kein Ausdruck. In einem Bedingungs-Kopf bleibt sie verboten, und zwar mit Absicht:
if (running = false) { ... } # Parse-Fehler
while (x = 1) { ... } # Parse-FehlerDas ist der klassische Vertipper = statt ==. Weil Zuweisungen nur in
Body-Positionen stehen duerfen, kann Antra ihn zuverlaessig melden:
Unexpected '=' in 'if' condition; '=' is assignment - use '==' to compareEbenso ungueltig bleibt eine Zuweisung mitten in einem groesseren Ausdruck
(f(x = 1)) und eine my/val-Deklaration als klammerloser Body — deren
Scope waere sofort wieder zu Ende.
Klammern um die Bedingung: Begrenzer oder Gruppierung#
Eine Klammer direkt nach if/unless/while kann zweierlei sein, und Antra
entscheidet das anhand dessen, was nach der schliessenden ) kommt:
- Schliesst dort die Bedingung ab (es folgt
then,{oderelse), ist die Klammer der Begrenzer der Bedingung — die klassische Formwhile (i < 10). - Folgt ein Operator, war die Klammer nur eine Gruppierung innerhalb des Ausdrucks, und die Bedingung laeuft weiter.
Damit brauchst du keine doppelten Klammern, nur um eine Rechen-Reihenfolge zu erzwingen:
if (a + b) % 2 == 0 then "gerade" else "ungerade" # (a + b) ist Gruppierung
if ((a + b) % 2 == 0) then ... # gleichwertig, nur klammrigerBeide sind erlaubt und bedeuten dasselbe. Wichtig ist die Gruppierung um
a + b selbst: % bindet staerker als +, a + b % 2 waere also a + (b % 2).
Der Grund fuer die Regel: if (x) then … soll die Klammer als reinen Begrenzer
lesen (wie while (x)), if (x) 1 dagegen bleibt der Fehler „kompakte Form
braucht then" — ein frisches Primary nach ) ist keine Fortsetzung des
Ausdrucks.
if als Ausdruck#
Für Umsteiger (z.B. von perl) ist wichtig: In Antra sind if-Formen oft Ausdrücke, nicht nur Statements.
my sign = if x > 0 then :pos else :otherWird ein if/unless als Wert benutzt (Deklarations- oder
Assignment-RHS), sollte es ein else haben: ohne else liefert der Ausdruck
still undef, wenn die Bedingung nicht greift — fast immer ein versteckter
Fehler. Die statische Pruefung meldet das als Warnung one-armed-if-value
(siehe Kapitel 17):
val x = if cond then compute() # warning: one-armed-if-value
val x = if cond then compute() else undef # ok: Fallback ist explizitAls reines Statement (Wert wird verworfen) bleibt die einarmige Form dagegen
voellig normal: if debug then println("...").
Zur Referenz gibt es also zwei Hauptformen, beide als Statement und als Ausdruck verwendbar:
- Block-Form mit optionalen Parens:
if cond { ... } else { ... }oderif (cond) { ... } else { ... } - Then-Form:
if cond then ... else ...
unless#
Dasselbe gilt fuer unless, die invertierte Form:
unless ready?() {
println("warte noch")
}Wichtig dabei: Optional sind nur die Klammern der if-/unless-Form selbst.
Die Klammern eines nullstelligen Funktionsaufrufs bleiben noetig. unless ready?
ist also aktuell kein Aufruf von ready?(), sondern nur der nackte
Funktionswert.
Header-Bindungen in if, unless und while#
Fuer if, unless und while gibt es eine kleine Header-Form
fuer lokale Bindungen, bei der Bindung und Bedingung getrennt bleiben:
if val user = find_user(id); defined? user {
println(user.name)
} else {
println("not found")
}
unless val user = find_user(id); defined? user {
println("not found")
} else {
println(user.name)
}
while val line = next_line(reader); defined? line {
println(line)
}Dieselben Header-Bindungen duerfen auch Destructuring und andere bekannte Pattern-Ziele tragen:
if val [user, msg] = find_user(id); defined? user {
println(user.name)
} else {
println("not found" <> msg)
}Wichtig fuer diese Form:
- erlaubt sind
my- undval-Bindings auch mit Destructuring und Patterns constist dort noch bewusst nicht erlaubt- der gebundene Name ist in Bedingung und Branch/Body sichtbar, aber nicht ausserhalb der gesamten Kontrollflussform
- bei
whileentsteht die Bindung pro Iteration frisch - ein Pattern-Mismatch wirft denselben Fehler wie normales Destructuring; er zaehlt nicht als stilles
false
Fuer Result- oder Option-artige Verzweigung ist diese Header-Form aber oft
nicht die beste Surface-Syntax, auch wenn Tagged-Patterns technisch erlaubt
sind. Als Leitidee ist sie staerker fuer "binde etwas, pruefe danach eine
normale Bedingung" gedacht. Klarer ist dort meist das Pattern-if.
Pattern-if: if %Ok[x] <- expr#
Wenn der Kontrollfluss schon in der Form des Rueckgabewerts steckt, ist die kleine Pattern-Branch-Form die naheliegende Wahl:
if %Ok[user] <- find_user(id) {
println(user.name)
} else %Err[msg] {
println("missing: " <> msg)
}Diese Form ist gedacht, wenn lokal genau ein Erfolgsfall und ein zweiter Pattern-Fall behandelt werden sollen.
Die ausfuehrlichere Header-Binding-Variante bleibt trotzdem sinnvoll, wenn der Zwischenwert bewusst einen Namen haben soll oder wenn die Branch-Logik nicht als Pattern-Branch formuliert werden soll:
if val result = find_user(id); Result.is_ok(result) {
val %Ok[user] = result
println(user.name)
} else {
val %Err[msg] = result
println("missing: " <> msg)
}Das Pattern-if gibt es auch als kompakte Ausdrucksform mit then, inklusive
optionaler Pattern-else-Kette:
val label = if %Ok[user] <- load() then user.name else "missing"
val code = if %Ok[value] <- parse() then 200
else %Err[msg] then -1
else 0Tag-Patterns tragen dabei — wie ueberall in der Sprache — das %-Sigil:
%Ok[user], nicht Ok[user].
while-Kurzformen mit <-#
Fuer lesenahe Schleifen gibt es kleine <--Kurzformen.
Die einfachere Form ist while name <- expr { ... } fuer APIs, die einen
rohen Erfolgswert liefern und mit undef terminieren:
while line <- next_line(reader) {
println(line)
}Wichtig dabei: Diese Form beendet nicht bei allgemeiner Truthiness, sondern
gezielt nur bei undef. Eine leere Zeile "" ist also weiterhin ein gueltiger
Wert und beendet die Schleife nicht.
Daneben gibt es die Pattern-Form, wenn der Kontrollfluss schon in der Rueckgabe selbst steckt:
while %Some[line] <- File.read_line(fh).! {
println(line)
}Und wenn der erste Nicht-Match noch explizit behandelt werden soll:
while %Ok[%Some[line]] <- File.read_line(fh) {
println(line)
} else %Ok[%None[]] {
println("eof")
} else %Err[err] {
println(err.message)
}Postfix-Formen#
Postfix-If#
println("debug") if debugDas ist knapp und praktisch für einfache Nebenbedingungen. Postfix-if und
-unless haengen an ganzen Statements (Expression-Statement, Assignment,
return, yield, last/next/redo); greift die Bedingung nicht, wird das
Statement einfach uebersprungen — es entsteht kein Wert.
Postfix-Pattern-If#
Die Postfix-Bedingung darf auch ein Pattern-Match sein, analog zu
if pattern <- expr. Bei einem Match laeuft das Statement mit den
Muster-Bindungen im Scope — die Bindung steht also links vom if und ist
trotzdem sichtbar:
fn load() -> %Some["User"]
println(user) if %Some[user] <- load() # -> User
x = user * 2 if %Some[user] <- load()
return err if %Err[err] <- attempt()Der Match-Wert wird genau einmal ausgewertet. Ohne Match passiert nichts.
unless ist die Spiegelform: das Statement laeuft im Nicht-Match-Fall —
dann natuerlich ohne Muster-Bindungen, es wurde ja nichts gebunden:
println("kein user") unless %Some[u] <- load()Blöcke#
do { ... }#
Wenn du in Antra mehrere Ausdrücke lokal gruppieren und dabei einen eigenen Scope haben willst, nimm dafür normalerweise do { ... }:
do {
my x = 10
println(x)
}Außerhalb des Blocks ist x nicht sichtbar.
Ein nackter { ... }-Block ist in Antra nicht einfach ein allgemeiner Scope-Block, sondern wird als Lambda-/Blockwert gelesen. Für einen "jetzt ausführen und letzten Wert zurückgeben"-Block ist do { ... } deshalb die richtige Form.
do { ... } als Ausdruck#
do ist nützlich, wenn du lokal mehrere Schritte brauchst, aber am Ende einen Wert zurückgeben willst:
my label = do {
my base = "User"
base <> " #42"
}Schleifen mit for#
Die <--Form#
Antra kennt ein for-Statement für typische Iteration. Die bevorzugte
Schreibweise benutzt <-:
for x <- [1, 2, 3] {
println(x)
}for benutzt nur noch <-. Das ist die direkte, lesbare Form fuer
Seiteneffekte. Wenn du transformieren willst, sind map und filter oft die
bessere Wahl; wenn du Werte erzeugen oder sammeln willst, sind
select (...) yield ... oder eine bewusste Materialisierung wie
[..select (...) yield ...] die passenderen Formen (siehe unten).
Der Schleifen-Body darf ein Block oder ein einzelner Ausdruck sein:
for x <- [1, 2, 3] println("expr:" <> str(x))Index und Key#
Im Alltag gibt es drei wichtige Formen:
for x <- seq {
println(x)
}
for x, i <- seq {
println(str(i) <> ":" <> str(x))
}
for seq {
println(str(_2) <> ":" <> str(_))
}Die zweite Form bindet zusaetzlich Index oder Key:
- bei Arrays, Strings und Ranges ist
_2bzw. der zweite Name der numerische Index - bei Maps ist
_2bzw. der zweite Name der Key
Die kurze Collection-Form#
Die kurze Collection-Form for seq { ... } ist praktisch, wenn du keinen
eigenen Namen einfuehren willst. Im Body gelten dann dieselben impliziten
Platzhalter wie bei Block-Lambdas fuer foreach:
_und_1sind der aktuelle Wert_2ist Index oder Key_argsund_paramssind ebenfalls vorhanden
Beispiel:
for %{ a: 10, b: 20 } {
println(str(_2) <> " => " <> str(_))
}Dieselbe Kurzform gibt es als Suffix, wie in Perl:
println(_) for 1..3 # 1, 2, 3
my total = 0
total += _ for [1, 2, 3] # wiederholt die ganze Zuweisung: 6
println(_2, ":", _) for ["a", "b"]Der Suffix wiederholt die ganze Anweisung, bei einer Zuweisung also
Ziel und Wert (n += x for x <- xs zählt n hoch). Weitere Klauseln
(if, mehrere Generatoren) brauchen einen Namen:
println(x) for x <- xs; if x > 0.
Gebunden werden nur die Platzhalter, die der Rumpf nennt: ein k += 1 for xs
legt pro Durchlauf keine Variable an.
Patterns links von <-#
Links von <- darf auch direkt ein Pattern stehen — inklusive Index-Binding:
for [name, age] <- rows {
println(name <> ":" <> str(age))
}
for %Ok[user] <- results {
println(user.name)
}
for %{name:, age:}, i <- people {
println(str(i) <> ":" <> name <> ":" <> str(age))
}Wichtig dabei: Diese Form ist strikt. Wenn ein Element nicht zum Pattern passt,
wirft die Schleife einen normalen Pattern-Mismatch-Fehler. Sie ist also keine
stille Filter- oder Skip-Schreibweise. Die weiche Variante, die Mismatches
ueberspringt, ist die select-Comprehension (siehe unten).
Früh aussteigen: return, last, next, redo#
Vier Schluesselwoerter verlassen den normalen Ablauf vorzeitig. Fuer
Umsteiger die Uebersetzung vorweg: Antra benutzt die Perl-Namen —
last ist break, next ist continue.
return#
return verlaesst die umgebende Funktion sofort, mit oder ohne Wert:
fn find_admin(users) -> {
for u <- users {
return u if u.admin
}
undef
}Wichtig fuer den Antra-Stil: Der letzte Ausdruck eines Funktionskoerpers
ist ohnehin der Rueckgabewert — ein explizites return am Ende ist also
unnoetig. return ist die Form fuer fruehe Ausstiege: Guard-Zeilen am
Funktionsanfang, Treffer in Schleifen, Fehlerfaelle.
last und next#
Beide steuern die innerste Schleife (while, Statement-for, und auch
das Built-in foreach):
lastbricht die Schleife ganz ab (wiebreak)nextspringt zum naechsten Durchlauf (wiecontinue)
my i = 0
while (i < 10) {
i = i + 1
next unless i % 2 == 0 # ungerade ueberspringen
last if i > 6 # ab 8 ist Schluss
println(i) # -> 2, 4, 6
}redo#
redo wiederholt den aktuellen Schleifendurchlauf, ohne die Bedingung
oder den Iterationsschritt neu auszuwerten (Perl-Erbe) — bei for bleibt die
Schleife also auf demselben Element stehen. Das ist selten noetig;
typischer Fall sind Retry-artige Schleifenkoerper:
my tries = 0
for job <- ["ok-job", "flaky-job"] {
tries = tries + 1
redo if job == "flaky-job" && tries < 4 # simulierter Retry
println(job <> " fertig nach " <> str(tries) <> " Versuch(en) gesamt")
}
# -> ok-job fertig nach 1 Versuch(en) gesamt
# -> flaky-job fertig nach 4 Versuch(en) gesamtDer zweite Durchlauf haengt dreimal auf "flaky-job" fest (tries 2, 3, 4),
ohne dass die Schleife zum naechsten Element weitergeht — erst als die
redo-Bedingung nicht mehr greift, laeuft der Body durch. Wer redo
einsetzt, muss selbst dafuer sorgen, dass sich im Body etwas aendert (hier
der tries-Zaehler), sonst entsteht eine Endlosschleife.
Zusammenspiel mit Postfix und Comprehensions#
Alle vier tragen die Postfix-Formen aus dem Postfix-Abschnitt — inklusive Pattern-Bedingung:
last if i >= 10
next unless ready
return -1 unless n > 0
return err if %Err[err] <- attempt()Zwei Abgrenzungen:
- In
select (...) yield ...-Comprehensions gibt es keinlast/next— dort uebernehmen Filter (if/unless-Clauses) undtake:diese Rollen. yield(Generatoren, unten) ist kein Ausstieg, sondern eine Pause: der Generator liefert einen Wert und laeuft beim naechstennext(...)weiter.
next, last und redo sind zugleich gueltige Variablennamen
([first, ..rest, last], %Emit[data, next]). Der Kontext entscheidet:
next.x, next[0], next(gen) oder next + 1 sind Ausdruecke. Ein
alleinstehendes next (auch mit Postfix-if) liest eine sichtbare
Variable dieses Namens und springt nur, wenn es keine gibt — Funktionen wie
last aus import Antra::Array.* zaehlen dabei nicht:
fn step(s) -> {
val next = s + 1
next # Rueckgabewert, kein Sprung
}
for x <- xs {
val next = step(x)
next # liest die Variable - die Schleife springt hier NICHT
}Der zweite Fall ist fast immer ein Versehen; --check meldet ihn als
loop-keyword-variable (siehe
Statische Pruefung).
Generatoren und Comprehensions (experimentell)#
gen fn und yield#
Generatoren sind der aktuelle kleine Einstieg in lazy Sequenzen jenseits von Ranges. Sie sind noch bewusst schmal, aber der Grundpfad ist schon benutzbar.
Die Form ist:
gen fn count_to(n) -> {
my i = 1
while (i <= n) {
yield i
i = i + 1
}
}Wichtig fuer die Lesart:
gen fndefiniert keine fertige Liste, sondern einen Produceryieldgibt jeweils genau einen Wert nach aussen- der Generator laeuft erst weiter, wenn ihn ein Consumer erneut anfordert
select-Comprehensions#
Fuer wertproduzierende Comprehensions gibt es die select-Form:
gen fn times_ten(xs) -> {
select (
x <- xs
) yield x * 10
}Diese Form ist im Moment noch absichtlich eng:
- eine oder mehrere Generator-Clauses
x <- seq; mitx, i <- seqbekommtidie Position (0, 1, ...), wie beimfor-Statement - Generator-Clauses duerfen links von
<-auch direkte Patterns wie[a, b],%Ok[v]oder[:host h, :port p, ..]tragen - optional ein oder mehrere Filter
if condoderunless cond - optional lokale Bindungen als
val y = ...,const y = ...oder kurzy = ... - dieselben Binding-Clauses duerfen auch als Pattern-Bindings wie
[a, b] = pair,{x, y} = point,%Ok[v] = itemoder[:host h, :port p, ..] = cfggeschrieben werden - auf derselben Zeile koennen mehrere Clauses in einer kompakten
select ( ... ; ... ) yield ...-Form mit;getrennt werden - fuer gruppierte oder mehrzeilige Clauses ist
select (...) yield exprdie bevorzugte Form
Die Position eines Elements kommt als zweiter Name dazu:
val labels = [..select (x, i <- ["a", "b", "c"]) yield "$i:$x"]
println(labels) # ['0:a', '1:b', '2:c']Filter und lokale Bindungen#
Mit Filter:
gen fn even_times_ten(xs) -> {
select (
x <- xs
if x % 2 == 0
) yield x * 10
}Negiert geht dieselbe Clause auch mit unless:
gen fn small_only(xs) -> {
select (
x <- xs
unless x > 10
) yield x
}Mit Zwischenbindung ueber mehrere Zeilen:
gen fn labeled_prefix(xs) -> {
select (
x <- xs
val y = "n=" <> str(x)
) yield y
}Die kuerzere Form ohne Praefix geht ebenfalls und meint dieselbe
val-artige Zwischenbindung: nicht neu bindbar, aber nicht eingefroren.
gen fn labeled_short(xs) -> {
select (
x <- xs
y = "n=" <> str(x)
) yield y
}Auch mehrere lokale Bindungen funktionieren in dieser kurzen Form:
gen fn decorated(xs) -> {
select (
x <- xs
y = x * 10
z = "n=" <> str(y)
) yield z
}Wichtig dabei: Diese Binding-Clauses sind keine normale Mutation. Innerhalb der
Comprehension bedeuten y = ... und val y = ... eine neue lokale
val-Bindung fuer die nachfolgenden Clauses und fuer den yield-Ausdruck:
- nicht neu bindbar
- aber nicht automatisch eingefroren
const y = ... bleibt die ausdruecklich staerkere Form und friert den Wert wie
sonst auch transitiv ein. Fuer normale Zwischenwerte ist deshalb in der Regel
y = ... oder val y = ... die passendere Wahl.
Binding-Clauses duerfen auch Patterns benutzen:
gen fn sums(pairs) -> {
select (
pair <- pairs
[a, b] = pair
sum = a + b
) yield sum
}Ein Pattern-Mismatch in solchen Binding-Clauses wirft weiterhin denselben Laufzeitfehler wie normales Destructuring ausserhalb der Comprehension.
Patterns in Clauses: weich vs. strikt#
Dieselbe direkte Pattern-Form wie im normalen for geht auch direkt in
der Generator-Clause von select. In select ist diese Form weich: Ein
Pattern-Mismatch wird uebersprungen statt als Fehler geworfen.
gen fn pair_sums_soft(pairs) -> {
select (
[left, right] <- pairs
) yield left + right
}
gen fn ok_values_soft(items) -> {
select (
%Ok[value] <- items
) yield value
}Wenn du striktes Matching brauchst, nimm eine getrennte Binding-Clause — die matcht hart und wirft bei Mismatch den normalen Destructuring-Fehler:
gen fn unwrap_ok_strict(items) -> {
select (
item <- items
%Ok[value] = item
) yield value
}(Ein hartes for-Statement mit yield im Body geht dafuer im aktuellen MVP
nicht: for-Bodies laufen intern als Lambda, und yield ist in Lambdas
innerhalb eines Generators noch nicht erlaubt — siehe die Grenzen unten.)
Merkregel: for ist hart, select ist weich.
Mehrere Generator-Clauses#
In der kompakten Form auf einer Zeile trennt ; die Clauses:
gen fn pairs_inline(xs, ys) -> {
select (x <- xs; y <- ys) yield x <> ":" <> y
}Die Prefix-Form ist die bevorzugte Schreibweise fuer mehrzeilige Clauses:
gen fn pairs_multiline(xs, ys) -> {
select (
x <- xs
y <- ys
) yield x <> ":" <> y
}Postfix-for und for (...) do#
Fuer reine Seiteneffekte gibt es die postfixe Form ohne yield:
println(name) for name <- peopleDiese Variante wird sofort komplett ausgefuehrt und ergibt als Ausdruck immer
undef. Sie ist also fuer Dinge wie Logging oder Ausgabe gedacht, nicht fuer
lazy Sequenzen oder into:-Materialisierung.
Dieselbe gruppierte Form gibt es auch als Prefix-Ausdruck mit explizitem
do:
for (
name <- people
if name != ""
) do println(name)
for (
name <- people
) do {
println("Name: " <> name)
}Materialisieren: [..], into: und collect#
Ein Aufruf einer gen fn oder eine nackte select-Comprehension liefert einen
Generatorwert, keine fertige Collection. Wenn du das Ergebnis sofort als Array
brauchst, materialisierst du die Sequenz explizit ueber [..select (...) yield ...]:
my arr = [..select (
x <- [1, 2, 3]
) yield x * 10]
my adults = [..select (
row <- rows
const [name, age] = row
if age >= 18
) yield name]Als explizite Langform derselben Materialisierung gibt es den Weg über die
into:-Option — je nach Ziel-Collection als Array, Set oder Map:
my arr2 = select (
x <- [1, 2, 3]
into: []
) yield x * 10
my seen = select (
x <- [1, 2, 2, 3]
into: %<>
) yield x
my by_name = select (
row <- rows
[name, age] = row
into: %{}
) yield [name, age]
my by_name2 = select (
row <- rows
[name, age] = row
into: %{}
) yield name => age
my arr3 = select (
x <- [1, 2, 3]
into: existing_array
) yield x * 10
my arr4 = select (
x <- [1, 2, 3]
into: [100]
) yield x * 10
my seen2 = select (
x <- [2, 3, 3, 4]
into: %<1, 2>
) yield x
my seen3 = select (
x <- [2, 3, 3, 4]
into: %Seen<1, 2>
) yield x
my by_name3 = select (
row <- rows
[name, age] = row
into: %{ "Ada" => 30, "Cara" => 40 }
) yield name => ageDieselbe Materialisierung kann auch explizit ueber collect(...) beschrieben
werden:
my arr = collect(select (
x <- [1, 2, 3]
) yield x * 10)
my adults = collect(select (
row <- rows
const [name, age] = row
if age >= 18
) yield name)Die Form bleibt absichtlich eng:
- fuer direkte Array-Materialisierung ist
[..expr]die allgemeine Form;[..1..5]spreadet eine Range auch ohne Klammern (ebensof(..1..3)) into:steht als letzte Option innerhalb vonselect (...), nach allen Clauses; einzeilig trennt;sie wie die Clauses:select (x <- xs; into: %<>) yield x- im jetzigen MVP sind
into: [],into: %<>,into: %{},into: targetmit einfachem Identifier sowie konkrete rohe und getaggte Array-, Set- und Map-Literale als Seed-Ziele implementiert into: %{}erwartet pro Schritt ein Paar. Dafuer gehen sowohlyield [key, value] ...als auchyield key => value ...; letzteres ist nur der allgemeine Pair-Sugar, spaetere Keys gewinnen bei Duplikateninto: targetund konkrete Collection-Literale verwenden den Seed nur als Startinhalt und liefern immer einen neuen Array-, Set- oder Map-Wert zurueck; das Seed-Binding selbst wird nicht still veraendert%Tag<...>ist ein Tagged Set Literal; bei Set-Seeds und linksgerichteten Set-Operationen wieSet.diff(...)odera \\ bbleibt der Tag der linken beziehungsweise Seed-Seite erhalten- fuer schon gebundene Generatorwerte bleibt weiter
[..gen]zustaendig - bei potentiell unendlichen Quellen bleibt die Materialisierung eager; fuer
einen endlichen Ausschnitt brauchst du also weiter explizit
take(...) - Konstruktoren wie
%Tag[...]bleiben normale Tagged-Array-Literale und bekommen keine eigene Comprehension-Sonderregel
Ein praktischer Fall dafuer ist eine kleine CSV-Vorschau, die direkt als Array gebraucht wird:
my rows = [
["Alice", "29", "Berlin"],
["Bob", "41", "Hamburg"],
["Cara", "35", "Koeln"]
]
my preview = [..select (
row <- rows
[name, age, city] = row
if num(age) >= 30
) yield name <> " (" <> city <> ")"]
my preview2 = select (
row <- rows
[name, age, city] = row
if num(age) >= 30
into: []
) yield name <> " (" <> city <> ")"
println(preview)
println(preview2)
my unique_cities = select (
row <- rows
[_, _, city] = row
into: %<>
) yield city
println(unique_cities)
my existing_names = ["Bestand"]
my names_with_seed = select (
row <- rows
[name, _, _] = row
into: existing_names
) yield name
println(existing_names)
println(names_with_seed)
my ages_by_name = select (
row <- rows
[name, age, _] = row
into: %{}
) yield name => age
println(ages_by_name)Wenn du dagegen nur einen begrenzten Ausschnitt eines groesseren oder sogar
unendlichen Generators brauchst, bleibt take(...) die passendere Form:
take(select (
row <- rows
[_, _, city] = row
) yield city, 2)Dasselbe gilt auch fuer andere Consumer wie map(...) oder fuer die explizite
Pipe-Form (select (...) yield ...) |> take(...). Nur nacktes yield expr bleibt
weiter auf gen fn beschraenkt.
take: für begrenzte Consumption#
Fuer begrenzte Consumption direkt im Header gibt es zusaetzlich take::
my first_three = select (
x <- 1..*
take: 3
into: []
) yield x * 10
for (
x <- 1..*
if x % 2 == 0
take: 3
) do println(x)
println(name) for name <- ["Ada", "Bob", "Cara"] take: 2Dabei gilt bewusst:
take:zaehlt produzierte Ergebnisse, nicht rohe Quell-Iterationen- Filter und weiche Pattern-Mismatches verbrauchen also kein
take:-Budget - in Prefix-Comprehensions stehen
take:undinto:am Ende des Headers nach allen Clauses take:kann mitselect (...) yield ...,for (...) do ...und der postfixen Seiteneffekt-Formexpr for ...verwendet werden, aber nicht mit harten Statement-for-Schleifentake:begrenzt dieselbe Comprehension direkt im Header und ist besonders fuer unendliche Quellen wie1..*praktisch
Generatoren konsumieren: next und das One-shot-Modell#
Ein Aufruf liefert einen Generatorwert, keine materialisierte Collection:
my g = count_to(3)
println(g)
println(next(g))
println(next(g))
println(next(g))
println(next(g))Die Ausgabe ist dabei bewusst opak und der Pull-API-Vertrag ist explizit:
<generator>
%Some[1]
%Some[2]
%Some[3]
%None[]next(...) liefert also kein neues Spezialobjekt, sondern die bekannte Tagged-
Array-Form %Some[...] | %None[]. In der Doku kann das als Option[T]
beschrieben werden, zur Laufzeit bleibt es aber bei Tagged Arrays.
Wenn du Werte einzeln ziehen willst, matchst du typischerweise genau auf diese beiden Formen:
my g = count_to(2)
match next(g) {
%Some[x] -> println("erstes Element: " <> x)
%None[] -> println("leer")
}
match next(g) {
%Some[x] -> println("zweites Element: " <> x)
%None[] -> println("leer")
}
match next(g) {
%Some[x] -> println("unerwartet: " <> x)
%None[] -> println("fertig")
}Das ist die direkte Form, wenn du einen Generator wirklich Schritt fuer Schritt steuern willst, zum Beispiel in einer REPL, bei Protokoll-Parsern oder bei einem Stream, den du jederzeit abbrechen moechtest.
Wenn du die Werte wirklich als Array brauchst, materialisierst du bewusst:
count_to(5) |> collect() |> println()
count_to(100) |> take(3) |> println()Das ergibt:
[1, 2, 3, 4, 5]
[1, 2, 3]Wichtig ist dabei das One-shot-Modell: Ein Generator merkt sich seinen Fortschritt.
my g = count_to(5)
println(take(g, 2))
println(collect(g))
println(collect(g))Das ergibt:
[1, 2]
[3, 4, 5]
[]Wenn du noch einmal von vorne anfangen willst, erzeugst du einen neuen Generator:
println(collect(count_to(3)))
println(collect(count_to(3)))Generatoren haengen absichtlich an derselben Sequence-Bruecke wie Ranges. Darum funktionieren auch die ueblichen Konsumenten direkt darauf:
count_to(4) |> map(|x| -> x * 10) |> println()
count_to(6) |> filter(|x| -> x % 2 == 0) |> println()
count_to(3) |> foreach &println
for x <- count_to(3) {
println(x)
}Wenn du beim Iterieren auch den Index brauchst, funktioniert das genauso wie bei Arrays:
for x, i <- count_to(3) {
println(str(i) <> ":" <> str(x))
}Fuer praktische Streams ist die Kombination mit Paket-APIs oft interessanter als
ein rein akademisches Zaehler-Beispiel. Mit Antra::Random.stream(...) kannst
du etwa aus einem deterministischen RNG-Wert einen echten Generator machen:
import Antra::Random
my rolls = Random.stream(Random.int(1, 6), Random.seed(42))
println(take(rolls, 5))So bleibt die Erzeugung lazy, aber alle normalen Sequence-Konsumenten wie
take(...), map(...), filter(...), foreach(...) und for ... <- ...
funktionieren weiter ohne Spezialfall.
Grenzen des aktuellen MVP#
yieldist nur ingen fnerlaubtyieldin verschachtelten Lambdas innerhalb eines Generators ist noch nicht erlaubt — das schliesst auchfor-Bodies ein, deren Body intern als Lambda laeuft;while-Schleifen undselect (...) yield ...sind die Formen, die innerhalb einergen fnproduzieren koennen- Generatoren sind one-shot; wer noch einmal von vorne laufen will, ruft die Generator-Funktion erneut auf
- weitergehende Formen wie
yield fromoder Coroutine-Semantik gibt es noch nicht
Wenn du den detaillierten heutigen Istzustand sehen willst, lies spec_generators.md.
use: der Rest des Blocks als Callback#
Grundform und Desugaring#
use ist spezieller, aber sehr mächtig: Es gibt den Rest eines Blocks als
Callback weiter. Damit lassen sich APIs mit "trailing block" linear lesbar
schreiben. Ein typischer Fall ist eine API, die den Rest des Blocks als
Callback bekommt:
fn preview(path) -> {
use fh <- File.with_open(path)
File.read(fh, 5)
}Hier erwartet File.with_open(path, callback) selbst einen Callback als letztes
Argument. use bindet also fh und reicht den Rest des Blocks implizit als
diesen Callback hinein. Die Lesart ist also ungefaehr:
fn preview(path) -> {
File.with_open(path, |fh| -> {
File.read(fh, 5)
})
}Muster links vom <-#
Links vom <- sind neben Namen auch Muster erlaubt — genau wie bei
Lambda-Parametern, denn zu einem Lambda-Parameter wird die Bindung ja
desugart:
fn with_some(f) -> f(%Some["User"])
use %Some[x] <- with_some()
println(x) # -> UserMehrere Bindungen entsprechen mehreren Callback-Argumenten. Ruft die API ihren Callback z.B. mit zwei Werten auf, matcht jede Bindung "ihr" Argument:
fn pair(f) -> f([1, 2], %Ok["y"]) # Callback bekommt ZWEI Argumente
use [a, b], %Ok[s] <- pair() # [a, b] matcht [1, 2], %Ok[s] matcht %Ok["y"]
println(str(a + b) <> s) # -> 3yDas ist dieselbe Desugaring-Lesart wie oben:
pair(|[a, b], %Ok[s]| -> {
println(str(a + b) <> s)
})Ein Pattern-Mismatch ist dabei ein harter Laufzeitfehler, kein stilles Ueberspringen — die Meldung nennt den Wert, mit dem der Callback tatsaechlich aufgerufen wurde:
Error: 'use' binding does not match the callback value: %None[]
Hint: the pattern left of '<-' in 'use' must match the value the callback is called with.use mit and_then#
Daneben gibt es den expliziten Result-Stil mit Result.and_then(...), zum
Beispiel use plan <- Result.and_then(JSON.parse(raw_json)), wenn mehrere
fallible Schritte bewusst als lineare Result-Kette lesbar bleiben sollen oder
wenn ein Schritt erst im Callback weiterverarbeitet werden muss.
Achtung beim Kombinieren mit Mustern: Result.and_then/Option.and_then
entpacken schon selbst — der Callback bekommt den nackten Wert, nicht die
%Some/%Ok-Huelle. use %Some[x] <- Option.and_then(...) ist deshalb fast
immer ein Mismatch; dort bindet man einen einfachen Namen (use x <- ...).
Zusaetzlich gibt es zwei getrennte Kurzformen, ohne use <- selbst umzudeuten:
val x = result_value.^— Propagation: packt%Ok/%Someaus und macht bei%Err/%Noneein frühes Return aus der umgebenden Funktion (wie Rust?).val x = maybe_value.? // fallback— Soft-Unwrap: liefert lokalundefstatt Kontrollfluss und paart sich mit//fuer einen Fallback.
Gegenueberstellung fuer Option — and_then verkettet und schliesst bei
%None[] kurz, .? liefert nur lokal einen Fallback (kein Kontrollfluss):
import Antra::Option as Option
fn lookup(m, k) -> if k in m then %Some[m[k]] else %None[]
# and_then: %None[] bricht die Kette ab
fn greet(m) -> {
use name <- Option.and_then(lookup(m, :name))
%Some["Hallo " <> name]
}
greet(%{:name => "Ada"}) # => %Some["Hallo Ada"]
greet(%{}) # => %None[]
# .?: hier und jetzt auspacken, sonst Fallback
fn name_or_default(m) -> lookup(m, :name).? // "Gast"
name_or_default(%{}) # => "Gast"Dasselbe fuer Result — and_then verkettet, .^ propagiert (fruehes Return
bei %Err). Beide Varianten liefern hier dasselbe Ergebnis:
import Antra::Result as Result
fn parse_port(s) -> if s == "" then %Err["leer"] else %Ok[num(s)]
# and_then-Kette
fn port_plus_one(s) -> {
use p <- Result.and_then(parse_port(s))
%Ok[p + 1]
}
# .^-Propagation (inline, ohne Callback)
fn port_plus_one_prop(s) -> {
val p = parse_port(s).^
%Ok[p + 1]
}
port_plus_one("8080") # => %Ok[8081]
port_plus_one_prop("") # => %Err["leer"]defer: Aufräumen beim Verlassen eines Blocks#
Form und Regeln#
defer merkt sich Code, der läuft, wenn der umgebende Block endet — egal wie:
normales Ende, return, last/next, .^-Propagation oder Exception. Es ist
für Aufräumarbeiten gedacht (Fenster schließen, Dateien, Sockets). Nach defer
steht ein Ausdruck oder ein Block für mehrere Schritte:
fn order() -> {
defer println("zuerst registriert, laeuft zuletzt")
defer {
println("block 1")
println("block 2")
}
println("body")
42
}
println(order())
# body
# block 1
# block 2
# zuerst registriert, laeuft zuletzt
# 42- Rückwärts: das zuletzt registrierte
deferläuft zuerst. - Nur erreichte: ein
deferhinter einem frühenreturnläuft nicht. - Pro Block: der Block ist der Funktionsrumpf, ein
do { }, einif-Zweig oder ein Schleifenrumpf — dort läuft es am Ende jedes Durchlaufs (wie in Zig, anders als in Go). - Wert: der Block liefert weiter den Wert seines letzten Statements; was
der
defer-Code liefert, wird verworfen. - Zuweisungen: nach
deferdarf auch eine Zuweisung stehen:defer n += 1,defer state.log := state.log <> ".". - Bedingt: ein Postfix-
if/unlessgehört zumdeferselbst, wie beireturn x if c:defer close(f) if openedregistriert das Aufräumen nur, wennopenedbeim Erreichen der Zeile gilt. - Fehler im
defer: die übrigendeferlaufen trotzdem. Verlässt der Block ohnehin mit einem Fehler, bleibt dieser maßgeblich; sonst wird der erste Fehler aus einemdeferzum Fehler des Blocks. - Oberste Ebene: ein
deferaußerhalb jeder Funktion läuft am Programmende, auch nach einem Laufzeitfehler.exit(code)beendet sofort, ohne diedefer.
defer ist ein reserviertes Schlüsselwort (my defer = 1 ist ein
Parse-Fehler). Direkt in einem gen fn ist es nicht erlaubt: ein Generator
läuft schrittweise und wird oft nie zu Ende gelesen.
fn early(n) -> {
defer println("cleanup $n")
return "small" if n < 10
"big"
}
println(early(1))
# cleanup 1
# smalldefer zusammen mit use#
use x <- f() macht den Rest des Blocks zum Callback von f. Ein defer
vor dem use läuft deshalb erst, wenn f fertig ist — bei einer
Hauptschleife wie RL.run also einmal nach der ganzen Schleife:
fn run(state, step) -> {
my s = state
for _i <- 1..3 { s = step(s) }
s
}
fn main_loop() -> {
defer println("einmal schliessen")
use s <- run(0)
println("frame $s")
s + 1
}
println(main_loop())
# frame 0
# frame 1
# frame 2
# einmal schliessen
# 3Ein defer nach dem use steht im Callback und liefe bei jedem Aufruf,
also jeden Frame. Der Checker warnt dort mit defer-in-use-callback
(Kapitel 17).
Fehlerbehandlung#
Warum gibt es try/catch ueberhaupt?#
Der bevorzugte Fehlerstil in Antra ist die Result-API: Funktionen geben
%Ok[...]/%Err[...] zurueck, der Aufrufer entscheidet per match,
Pattern-if oder .^ weiter (ausfuehrlich in
Kapitel 11). try/catch sollte dagegen
spaerlich eingesetzt werden — es existiert aus zwei Gruenden:
- Die Laufzeit selbst wirft.
die(...), Pattern-Mismatches, Division durch null und viele Builtins enden als Exception — ebenso die nativen Module (Antra::Native::*) im Fehlerfall. Low-Level-Code an dieser Host-Grenze braucht einen Fangmechanismus, sonst reisst jeder tiefe Fehler das ganze Skript. - Als Uebergang zwischen den Stilen. Ein Skript-Top-Level faengt grob mit
try/catch, waehrend die Bibliotheken darunter sauberResultsprechen; umgekehrt wechseltthrowvon Result- in Exception-Stil.
Fuer den haeufigsten Fall — einen werfenden Aufruf in die Result-Welt holen —
brauchst du nicht einmal selbst try/catch zu schreiben; Result.try(...)
ist genau diese Bruecke:
import Antra::Result as Result
my r = Result.try(|| -> 1 / 0)
println(r) # %Err["Error: Division by zero"]Faustregel: eigene APIs geben Result zurueck; try/catch gehoert an die
Ränder — Skript-Top-Level, Host-Grenzen (native Module, FFI), Test-Helfer.
try / catch / throw#
try {
risky()
} catch {
%Err[msg] -> println(msg)
msg -> println("Fehler: " <> msg)
}throw nimmt wie return ein Postfix-if/unless:
throw "negative" if n < 0.
Auch hier arbeitet Antra mit Pattern Matching. catch ist also kein stumpfer
Fehlerstring-Handler, sondern kann verschiedene Fehlerformen unterscheiden.
Das ist nützlich, weil Fehlerbehandlung dadurch dieselbe Sprache spricht wie
der Rest der Sprache:
- Werte haben Formen
- Fehler haben Formen
- diese Formen können gematcht werden
try ist selbst ein Ausdruck und liefert den Wert des erfolgreichen Blocks
oder des gewählten catch-Arms. String-Patterns matchen dabei auch mit
Konkatenation:
try {
throw "Type error: bad input"
} catch {
"Type error: " <> rest -> println("Typfehler: " <> rest)
e -> println("Unbekannt: " <> e)
}throw selbst ist einfach:
throw "kaputt"Für einfache Fälle reicht ein String. In größeren Programmen ist ein strukturierter Fehlerwert oft robuster.
String-Patterns vs. %Error{...}-Patterns#
Wichtig fuer die aktuelle Fehler-Surface:
- String-Patterns in
catchbleiben weiter message-orientiert %Error{...}-Patterns matchen gegen den rohen Error-Wert- eine nackte Variable wie
e -> ...bindet beidie(...)oderthrow error(...)ebenfalls den rohen Error-Wert - Map-Patterns in
catchsind dabei nicht automatisch offen - wenn du nur einen Teil der Error-Map matchen willst, musst du wie sonst auch
..oder..restbenutzen
Das ist absichtlich konsistent mit normalen Map-Patterns im Rest der Sprache.
Bei strukturierten Fehlerwerten ist der Unterschied zwischen String-Pattern und Map-Pattern wichtig:
try {
throw error("Type error: bad input", [site: :caller])
} catch {
"Type error: bad input" -> println("string match")
}Das String-Pattern oben arbeitet weiter auf der Fehler-Message.
Wenn du stattdessen den rohen %Error{...}-Wert sehen willst, matchst du als
Map-Pattern oder bindest ihn an eine Variable:
try {
throw error("Type error: bad input", [site: :caller])
} catch {
%Error{message: msg, site: :caller, ..} -> println(msg)
}
try {
die("Type error: bad input", [site: :caller])
} catch {
err -> println(err.message)
}Ohne .. oder ..rest ist ein %Error{...}-Pattern geschlossen. Das hier ist
also absichtlich zu streng, solange der Fehlerwert weitere Felder wie
kind, line oder trace traegt:
try {
throw error("oops")
} catch {
%Error{message: msg} -> println(msg)
}Fuer Teilmatches ist deshalb in der Praxis fast immer eine offene Form die richtige Wahl:
%Error{message: msg, ..}
%Error{message: msg, ..rest}context(...)#
Wenn du denselben Diagnose-Kontext wie warn(...), die(...) oder error(...)
als normale Daten lesen willst, gibt es dafuer context(...):
println(context().line)
println(context([site: :caller]).line)
println(context([trace: true]).trace)context(...) wirft nicht und schreibt nichts nach STDERR. Es liefert nur
eine normale Map mit Feldern wie name, line, site und optional trace.
try mit Result-Denke kombinieren#
Viele Bibliotheken liefern %Ok[...] oder %Err[...]. Das passt gut zu Pattern Matching:
match parse(text) {
%Ok[value] -> value
%Err[msg] -> throw msg
}Eine ausfuehrliche Entscheidungshilfe dazu, wann match, if val ...,
%Ok[...]/%Err[...], [value, err], throw, die(...), error(...),
warn(...) und context(...) die passende Form sind, steht im eigenen Kapitel
11-fehlerstile-und-result-apis.md.
Propagation mit .^ — Kontrollfluss im Operator#
Wer viele Result-liefernde Schritte hintereinander hat, will nicht nach jedem
Schritt ein match schreiben. Dafuer gibt es den harten
Propagations-Operator .^ — und der ist echter Kontrollfluss, auch wenn
er wie ein Postfix-Operator aussieht:
%Ok[x]/%Some[x]packt er aus: der Ausdruck ergibtx, es geht normal weiter%Err[e]/%None[]beendet die umgebende Funktion sofort und reicht den Fehlfall unveraendert an den Aufrufer weiter — wie ein verstecktesreturn
fn load(id) -> if id > 0 then %Ok[%{name: "Ada"}] else %Err["not found"]
fn show(id) -> {
val user = load(id).^ # %Err? -> show returnt hier sofort %Err[...]
%Ok[user.name] # nur der Ok-Pfad kommt hier an
}
println(show(1)) # %Ok["Ada"]
println(show(0)) # %Err["not found"] - durchgereichtDie Lesart: alles hinter einem .^ ist der Happy Path. Die Fehlfaelle
laufen unsichtbar nach oben durch, bis sie jemand behandelt (typischerweise
mit match oder if %Ok[...] <- an der Stelle, wo eine Entscheidung
faellt). Das ist dieselbe Idee wie ? in Rust.
Zwei Dinge dazu:
.^ist nicht auf Result/Option festverdrahtet, sondern laeuft ueber dasPropagate-Protokoll (fn __propagate__(value)); eigene Typen koennen also mitmachen- weil
.^die Funktion verlassen kann, gehoert es in Funktionen, die selbst einen Fehlfall zurueckgeben koennen — der Aufrufer bekommt sonst unbehandelte%Err[...]-Werte
Abgrenzung: .? ist kein Kontrollfluss#
Der weiche Bruder .? sieht aehnlich aus, tut aber etwas grundlegend
anderes: Er ist rein wertorientiert und lokal. %Ok[x]/%Some[x] wird
zu x, %Err[e]/%None[] wird zu undef — die Funktion laeuft ganz normal
weiter, nichts springt. Deshalb ist sein natuerlicher Partner das
Default-//:
fn first_or(xs, fallback) -> head(xs).? // fallback
println(first_or(["a", "b"], "leer")) # a
println(first_or([], "leer")) # leerAls Merkregel:
| Operator | Fehlfall wird zu | Springt? | Typische Paarung |
|---|---|---|---|
.^ | fruehes Return der Funktion | ja — nicht-lokal | match beim Aufrufer |
.? | undef an Ort und Stelle | nein — lokal | // fallback |
.! | geworfene Exception | ja — per Throw | try/catch oder Absturz |
Kurz: .^ entscheidet "dieser Pfad endet hier", .? entscheidet "dieser
Wert ist eben leer". Nur das erste ist Kontrollfluss; das zweite ist ein
Wert-Operator und steht hier nur zur Abgrenzung.
Harter Abbruch mit .! — wirf statt weiterzureichen#
.! ist die dritte Ecke des Tripels. Wie .^ packt es %Ok[x]/%Some[x]
aus, aber im Fehlfall wirft es eine Exception, statt den Fehler als Wert
nach oben zu reichen: %Err[e] wirft e, %None[] wirft eine
Unwrap-Meldung.
val port = parse_port(s).! # %Ok? -> Wert; %Err? -> ExceptionDas ist der Operator fuer "hier ist ein Fehler ein Bug oder unmoeglich, ich
will abstuerzen" — der Gegenpol zu .^ ("reiche den Fehler als Wert weiter").
Er loest die verstreuten foo!-Stdlib-Varianten am Use-Site ab: statt
einer eigenen parse!-Funktion schreibt man parse(s).!. Wie die beiden
Geschwister laeuft .! ueber ein Protokoll (UnwrapOrThrow,
fn __unwrap_bang__(value)), eigene Typen koennen also mitmachen und ihren
eigenen Wurf-Wert bestimmen.
.! und .^ sehen nur im Root aehnlich aus (beide beenden das Programm
mit einem Fehler). In Funktionen sind sie verschieden: .^ gibt den Fehlfall
als normalen Wert zurueck (mit match behandelbar), .! sprengt den Stack
per Exception (nur mit try/catch fangbar).
Leitlinien#
Lesbare Entscheidungslogik#
Ein häufiger Anfängerfehler in funktionalen Sprachen ist, alles mit cleveren Einzeilern ausdrücken zu wollen. In Antra musst du das nicht.
Nimm ruhig:
iffür klare Verzweigungenmatchfür Form-unterscheidungforfür sichtbare Iteration- Blöcke für lokale Zwischenschritte
Die Sprache bleibt auch dann idiomatisch.
Referenz: Formen von if#
| Form | Beispiel | Position |
|---|---|---|
| Block-Form | if cond { ... } else { ... } | Statement und Ausdruck |
| Kompakte Form | if cond then a else b | Statement und Ausdruck |
| Pattern-if (Block) | if %Ok[x] <- expr { ... } else %Err[e] { ... } | Statement |
| Pattern-if (kompakt) | if %Ok[x] <- expr then a else b | Statement und Ausdruck |
| Header-Bindung | if val x = expr; cond { ... } | Statement |
| Postfix | stmt if cond / stmt if pattern <- expr | Statement-Modifier |
unless | jeweils die invertierte Schwester | wie if |
Merkhilfen:
thennur vor einem einzelnen Ausdruck oder Statement, nie vor{ ... }- als Wert benutzt braucht ein
ifeinelse, sonst droht stillesundef - Tag-Patterns immer mit
%-Sigil:%Ok[x]
