Antra

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 Form

Ein 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 + n

Das gilt genauso fuer Match-Arme (siehe Kapitel 7) und ist bei Funktionskoerpern seit jeher so:

fn set(v) -> counter = v

Sobald 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-Fehler

Das 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 compare

Ebenso 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, { oder else), ist die Klammer der Begrenzer der Bedingung — die klassische Form while (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 klammriger

Beide 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 :other

Wird 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 explizit

Als 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 { ... } oder if (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- und val-Bindings auch mit Destructuring und Patterns
  • const ist dort noch bewusst nicht erlaubt
  • der gebundene Name ist in Bedingung und Branch/Body sichtbar, aber nicht ausserhalb der gesamten Kontrollflussform
  • bei while entsteht 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 0

Tag-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 debug

Das 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 _2 bzw. der zweite Name der numerische Index
  • bei Maps ist _2 bzw. 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 _1 sind der aktuelle Wert
  • _2 ist Index oder Key
  • _args und _params sind 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.

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):

  • last bricht die Schleife ganz ab (wie break)
  • next springt zum naechsten Durchlauf (wie continue)
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) gesamt

Der 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 kein last/next — dort uebernehmen Filter (if/unless-Clauses) und take: diese Rollen.
  • yield (Generatoren, unten) ist kein Ausstieg, sondern eine Pause: der Generator liefert einen Wert und laeuft beim naechsten next(...) 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 fn definiert keine fertige Liste, sondern einen Producer
  • yield gibt 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; mit x, i <- seq bekommt i die Position (0, 1, ...), wie beim for-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 cond oder unless cond
  • optional lokale Bindungen als val y = ..., const y = ... oder kurz y = ...
  • dieselben Binding-Clauses duerfen auch als Pattern-Bindings wie [a, b] = pair, {x, y} = point, %Ok[v] = item oder [:host h, :port p, ..] = cfg geschrieben werden
  • auf derselben Zeile koennen mehrere Clauses in einer kompakten select ( ... ; ... ) yield ...-Form mit ; getrennt werden
  • fuer gruppierte oder mehrzeilige Clauses ist select (...) yield expr die 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 <- people

Diese 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 => age

Dieselbe 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 (ebenso f(..1..3))
  • into: steht als letzte Option innerhalb von select (...), nach allen Clauses; einzeilig trennt ; sie wie die Clauses: select (x <- xs; into: %<>) yield x
  • im jetzigen MVP sind into: [], into: %<>, into: %{}, into: target mit einfachem Identifier sowie konkrete rohe und getaggte Array-, Set- und Map-Literale als Seed-Ziele implementiert
  • into: %{} erwartet pro Schritt ein Paar. Dafuer gehen sowohl yield [key, value] ... als auch yield key => value ...; letzteres ist nur der allgemeine Pair-Sugar, spaetere Keys gewinnen bei Duplikaten
  • into: target und 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 wie Set.diff(...) oder a \\ b bleibt 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: 2

Dabei 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: und into: am Ende des Headers nach allen Clauses
  • take: kann mit select (...) yield ..., for (...) do ... und der postfixen Seiteneffekt-Form expr for ... verwendet werden, aber nicht mit harten Statement-for-Schleifen
  • take: begrenzt dieselbe Comprehension direkt im Header und ist besonders fuer unendliche Quellen wie 1..* 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#

  • yield ist nur in gen fn erlaubt
  • yield in verschachtelten Lambdas innerhalb eines Generators ist noch nicht erlaubt — das schliesst auch for-Bodies ein, deren Body intern als Lambda laeuft; while-Schleifen und select (...) yield ... sind die Formen, die innerhalb einer gen fn produzieren koennen
  • Generatoren sind one-shot; wer noch einmal von vorne laufen will, ruft die Generator-Funktion erneut auf
  • weitergehende Formen wie yield from oder 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)
    })
}

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)                        # -> User

Mehrere 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)            # -> 3y

Das 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/%Some aus und macht bei %Err/%None ein frühes Return aus der umgebenden Funktion (wie Rust ?).
  • val x = maybe_value.? // fallback — Soft-Unwrap: liefert lokal undef statt 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 defer läuft zuerst.
  • Nur erreichte: ein defer hinter einem frühen return läuft nicht.
  • Pro Block: der Block ist der Funktionsrumpf, ein do { }, ein if-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 defer darf auch eine Zuweisung stehen: defer n += 1, defer state.log := state.log <> ".".
  • Bedingt: ein Postfix-if/unless gehört zum defer selbst, wie bei return x if c: defer close(f) if opened registriert das Aufräumen nur, wenn opened beim Erreichen der Zeile gilt.
  • Fehler im defer: die übrigen defer laufen trotzdem. Verlässt der Block ohnehin mit einem Fehler, bleibt dieser maßgeblich; sonst wird der erste Fehler aus einem defer zum Fehler des Blocks.
  • Oberste Ebene: ein defer außerhalb jeder Funktion läuft am Programmende, auch nach einem Laufzeitfehler. exit(code) beendet sofort, ohne die defer.

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
# small

defer 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
# 3

Ein 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:

  1. 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.
  2. Als Uebergang zwischen den Stilen. Ein Skript-Top-Level faengt grob mit try/catch, waehrend die Bibliotheken darunter sauber Result sprechen; umgekehrt wechselt throw von 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 catch bleiben weiter message-orientiert
  • %Error{...}-Patterns matchen gegen den rohen Error-Wert
  • eine nackte Variable wie e -> ... bindet bei die(...) oder throw error(...) ebenfalls den rohen Error-Wert
  • Map-Patterns in catch sind dabei nicht automatisch offen
  • wenn du nur einen Teil der Error-Map matchen willst, musst du wie sonst auch .. oder ..rest benutzen

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 ergibt x, es geht normal weiter
  • %Err[e] / %None[] beendet die umgebende Funktion sofort und reicht den Fehlfall unveraendert an den Aufrufer weiter — wie ein verstecktes return
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"] - durchgereicht

Die 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 das Propagate-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"))           # leer

Als Merkregel:

OperatorFehlfall wird zuSpringt?Typische Paarung
.^fruehes Return der Funktionja — nicht-lokalmatch beim Aufrufer
.?undef an Ort und Stellenein — lokal// fallback
.!geworfene Exceptionja — per Throwtry/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? -> Exception

Das 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:

  • if für klare Verzweigungen
  • match für Form-unterscheidung
  • for für sichtbare Iteration
  • Blöcke für lokale Zwischenschritte

Die Sprache bleibt auch dann idiomatisch.

Referenz: Formen von if#

FormBeispielPosition
Block-Formif cond { ... } else { ... }Statement und Ausdruck
Kompakte Formif cond then a else bStatement und Ausdruck
Pattern-if (Block)if %Ok[x] <- expr { ... } else %Err[e] { ... }Statement
Pattern-if (kompakt)if %Ok[x] <- expr then a else bStatement und Ausdruck
Header-Bindungif val x = expr; cond { ... }Statement
Postfixstmt if cond / stmt if pattern <- exprStatement-Modifier
unlessjeweils die invertierte Schwesterwie if

Merkhilfen:

  • then nur vor einem einzelnen Ausdruck oder Statement, nie vor { ... }
  • als Wert benutzt braucht ein if ein else, sonst droht stilles undef
  • Tag-Patterns immer mit %-Sigil: %Ok[x]

Quelle dieser Seite