Sprache
Pattern Matching erklärt
Pattern Matching ist eines der wichtigsten Konzepte in Antra. Wenn du es einmal verstanden hast, werden viele andere Sprachfeatures sofort klarer.
Destructuring: der vertraute Einstieg#
Bevor es um Pattern Matching im Allgemeinen geht, der Teil, den du wahrscheinlich schon kennst: Destructuring — mehrere Namen in einem Schritt aus einem zusammengesetzten Wert binden, wie in JavaScript oder Python.
my [first, second] = [10, 20]
my %{name: name, age: age} = %{name: "Ada", age: 36}Das ist in Antra aber mehr als syntaktischer Zucker: Die Struktur wird
gleichzeitig geprüft, und die Form links vom = ist ein vollwertiges
Pattern — derselbe Mechanismus, um den es im Rest dieses Kapitels geht. Alles,
was du hier siehst, funktioniert später identisch in match, in
Funktionsklauseln und in for-Schleifen.
Array-Destructuring#
my [first, ..rest] = [1, 2, 3, 4]first wird 1, rest wird [2, 3, 4]. Antra kann auch Interior-Gaps:
my [first, ..middle, last] = [1, 2, 3, 4]Das ist nützlich, wenn du Präfix und Suffix brauchst, aber die Mitte variabel ist.
Map-Destructuring#
my %{name: name, age: age} = userBei Tagged Maps geht das ebenfalls:
my %User{name: name} = userFuer Keys, die kein bindbarer Variablenname sind, braucht die Kurzform ein explizites Ziel:
my %{x-model: model} = %{x-model: "gpt"}Set-Destructuring#
Sets haben keine Positionen und keine Keys. Darum laeuft ihr Destructuring ueber Set-Patterns (Details weiter unten):
my %<1, 2, ..rest> = ids
const %Seen<1, ..rest> = seenDabei gilt:
%<1, 2>ist exakt%<1, 2, ..>erlaubt weitere Elemente%<1, 2, ..rest>bindet den uebrigen Rest als Set- bei
%Tag<...>muss der Tag passen
Bewusst nicht erlaubt sind im Moment freie Binder wie %<x> oder %<x, 2>,
weil Sets ungeordnet sind und solche Formen schnell mehrdeutig werden.
Keyword-Array-Destructuring#
my [host: host, port: port] = [host: "localhost", port: 8080]Das ist besonders praktisch für Optionslisten. Auch hier brauchen unhandliche Keys ein explizites Ziel:
my [user/id: user_id, ready?: ready] = [user/id: "42", ready?: true]Was bei einem Mismatch passiert#
Bei einer Bindung gibt es keinen zweiten Zweig: Passt die Form nicht, ist
das ein Laufzeitfehler. Nimm Destructuring also dort, wo ein Fehlschlag
wirklich ein Programmfehler waere — fuer "kann passen oder nicht" gibt es
match und if pattern <- (spaeter in diesem Kapitel).
Referenz: weitere Destructuring-Formen#
my {name, age} = %{name: "Alice", age: 30}
my %{name: n, age: a} = %{name: "Alice", age: 30}
const %{name:, age:} = %{name: "Alice", age: 30}
my %{"status" => "ok", body: b} = %{"status" => "ok", body: "hello"}Auch Tagged Arrays und Keyword Arrays lassen sich destrukturieren:
my %Ok[result] = %Ok[42]
my [:host h, :port p] = [host: "localhost", port: 8080]
const [:host h, :port p = 3306, :db d, ..] = kwVom Destructuring zum Pattern Matching#
Destructuring beantwortet nur eine Frage: "Zieh die Teile heraus — die Form stimmt schon." Pattern Matching stellt die interessantere Frage: "Welche Form hat der Wert — und was tue ich in jedem Fall?" Konkret kommt dreierlei dazu:
Patterns koennen Bedingungen enthalten. Nicht nur Namen, auch Literale und Tags:
%Ok[x]verlangt den TagOk,[1, x]verlangt eine1an erster Stelle. Ein Pattern kann also ablehnen — beim Destructuring waere das ein Fehler, im Matching ist es eine Information.Mehrere Patterns konkurrieren. In
matchund in Funktionsklauseln stehen Alternativen untereinander, und der erste Treffer gewinnt — der Mismatch eines Arms ist kein Fehler, sondern der Weg zum naechsten Arm:match result { %Ok[value] -> value %Err[msg] -> fallback(msg) }Guards verfeinern mit
when. Wenn die Form allein nicht reicht, prueft ein Guard zusaetzlich eine Bedingung:x when x > 3 -> ....
Destructuring ist also der Spezialfall "genau ein Pattern, Mismatch ist ein Bug". Der Rest des Kapitels zeigt den allgemeinen Fall — dieselben Formen, aber mit Auswahl.
Grundlagen#
Die Grundidee#
Statt einen Wert erst anzunehmen und danach mit if oder switch auseinanderzunehmen, beschreibst du direkt die Form, die du erwartest.
fn greet(%{name: name}) -> "Hallo " <> name
greet(%{name: "Ada"}) # => "Hallo Ada"Das Pattern %{name: name} sagt:
- der Wert muss eine Map sein
- sie muss einen Key
namehaben - dessen Inhalt wird an
namegebunden
Ein Pattern ist keine Expression#
Das ist wichtig für Einsteiger:
- Eine Expression konstruiert oder berechnet einen Wert.
- Ein Pattern beschreibt, welche Form ein bereits vorhandener Wert haben muss.
Dieselbe Oberfläche kann je nach Kontext verschieden gelesen werden:
%Ok[42] # Expression: konstruiert einen Wert
fn f(%Ok[x]) -> x # Pattern: matcht einen WertVariablen-Bindings und _#
Wenn in einem Pattern ein normaler Name steht, wird der gematchte Wert an diesen Namen gebunden.
fn echo(x) -> x
echo(42) # => 42Wenn dich ein Teil nicht interessiert, nimm _:
fn first([x, _]) -> xEin Name mit _-Praefix bindet dagegen ganz normal — das Praefix ist nur
ein Signal an Leser und statische Pruefung: "dieser Wert ist hier bewusst
uninteressant", die unused-Warnung bleibt still (siehe
Kapitel 17). Praktisch, wenn die
Form wichtig ist, der Wert aber (meistens) nicht:
fn ok?(%Ok[_value]) -> true
fn ok?(x) -> falseDie Regel ist damit ueberall dieselbe, wie in Rust oder Erlang: nur das nackte
_ verwirft; _name bindet, darf also bei Bedarf doch gelesen werden — etwa
voruebergehend beim Debuggen.
Ein Name im Pattern bindet neu — er vergleicht nicht#
Das ist der haeufigste Stolperstein. Wenn im Pattern ein Name steht, den es schon gibt, wird nicht gegen dessen Wert verglichen. Der Name wird lokal neu gebunden, und das Pattern matcht alles:
my x = 1
println(match 2 {
x -> "bound:" <> str(x)
})
# => "bound:2" — nicht etwa "kein Match"Einen Pin-Operator wie in Elixir gibt es nicht. Wenn du wirklich vergleichen willst, nimm ein Literal oder einen Guard:
match value {
1 -> "genau eins" # Literal
v when v == x -> "gleich wie x" # Guard
_ -> "etwas anderes"
}Denselben Namen zweimal in derselben Signatur zu verwenden, ist dagegen ein harter Fehler — mit genau diesem Hinweis:
Variable 'x' is already defined in this scope while binding a function parameter list
Reusing a name in the same parameter or pattern creates a new binding, not an equality check
Use a fresh binding name and compare with 'when'Literal-Patterns#
Ein Pattern kann einfach ein konkreter Literalwert sein:
fn describe(0) -> "null"
fn describe(1) -> "eins"
fn describe(n) -> "etwas anderes: " <> str(n)
describe(0) # => "null"
describe(1) # => "eins"
describe(7) # => "etwas anderes: 7"Die ersten beiden Klauseln matchen nur genau 0 bzw. 1.
Nicht nur ganze Zahlen funktionieren als Literal:
| Art | Beispiel |
|---|---|
| Ganze Zahlen, auch negativ | fn t(0), fn t(-1) |
| Dezimalzahlen, auch negativ | fn t(1.5), fn t(-2.5) |
| Strings | fn t("hi") |
| Keywords | fn t(:ok) |
| Booleans | fn t(true), fn t(false) |
undef | fn t(undef) |
Dezimalzahlen sind intern rationale Zahlen, verglichen wird deshalb der Wert
und nicht die Schreibweise — 1.5 matcht auch das Ergebnis von 3.0 / 2.
fn t(0) -> "zahl"
fn t("hi") -> "string"
fn t(:ok) -> "keyword"
fn t(true) -> "bool"
fn t(undef) -> "undef"
fn t(x) -> "anderes"Ein Literal ist auch als Map-Wert nuetzlich, um nach einem Diskriminator zu unterscheiden:
fn render(%{kind: "user", name: name, ..}) -> "User: " <> name
fn render(%{kind: "admin", ..}) -> "Admin"Patterns nach Datenform#
Array-Patterns#
fn first([x, ..]) -> x
fn pair_sum([a, b]) -> a + b
first([10, 20, 30]) # => 10
pair_sum([2, 3]) # => 5Das ist einer der größten Unterschiede zu vielen Mainstream-Sprachen: Die Struktur des Arrays steht direkt im Parameter.
Interior-Gap und Rest#
Antra unterstützt auch Muster wie:
fn first_last([first, ..middle, last]) -> [first, last]
first_last([1, 2, 3, 4]) # => [1, 4]Das bedeutet nicht Magie, sondern nur:
- nimm das erste Element
- nimm das letzte Element
- alles dazwischen kommt in
middle
Atom-Literale als Tag (Tagged Tuples)#
Ein :atom an einer Array-Position ist ein Literal, das genau diesen Atom
matcht. Das ist die idiomatische Form fuer getaggte Tupel wie [:ok, wert] oder
[:err, grund]:
match ergebnis {
[:ok, value] -> value # feste Laenge
[:ok, value, ..] -> value # Tag + Rest offen
[:ok, ..] -> :done # nur der Tag zaehlt
[:err, reason, ..] -> throw reason
_ -> :unknown
}Das gilt in jeder Position — auch fuehrend mit weiteren Bindungen dahinter:
fn move([:pawn, from, to]) -> from <> "->" <> toWichtig zur Abgrenzung von Keyword-Array-Patterns (naechster Abschnitt): :key
ist nur dann ein Keyword-Key, wenn direkt eine Bindung oder ein =-Default
folgt ([:host h, ..]). Steht hinter dem Atom ,, ] oder .., ist es ein
Atom-Literal ([:ok, ..]). Wer aus einem Keyword-Array binden will und keinen
Extra-Namen braucht, nutzt die Colon-Suffix-Kurzform [host:, ..].
Keyword-Array-Patterns#
Neben normalen Array-Patterns gibt es auch Keyword-Array-Patterns fuer Werte wie
[host: "localhost", port: 80]:
fn show([:host host, :port port]) -> host <> ":" <> str(port)
show([:host "localhost", :port 8080]) # => "localhost:8080"Mit as opts kannst du die effektiven Optionen direkt weiterverwenden:
fn connect([:host host = "localhost", :port port = 80] as opts) -> host <> ":" <> str(port)
connect() # => "localhost:80"
connect(host: "db") # => "db:80"
connect(port: 5432) # => "localhost:5432"Die Aufrufregeln fuer Keyword-Array-Parameter sind in 06-collections.md beschrieben. Hier geht es nur um die Pattern-Seite:
fn connect([:host host = "localhost", :port port = 80] as opts) -> host <> ":" <> str(port)
connect(host: "db", port: 5432) # => "db:5432"
fn render([pretty: = true, indent: = 4] as opts, cb) -> cb(opts)
render() { |opts| dd(opts) }
# => [[:pretty, true], [:indent, 4]]
Wenn alle Keys Defaults haben und der Keyword-Array-Parameter wirklich der
letzte Parameter ist, kannst du [] beim Aufruf weglassen. Wenn danach noch
ein Callback folgt, kann [] ebenfalls weggelassen werden:
fn render([pretty: = true, indent: = 4] as opts) -> dd(opts)
render() # => [[:pretty, true], [:indent, 4]]
render(indent: 8) # => [[:indent, 8], [:pretty, true]]
my render_fn = |[pretty: = true, indent: = 2] as opts| -> dd(opts)
render_fn([]) # => [[:pretty, true], [:indent, 2]]
render_fn(pretty: false) # => [[:pretty, false], [:indent, 2]]
fn render_with_cb(node, [pretty: = true, indent: = 4] as opts, cb) -> cb(node, opts)
render_with_cb(page) {
|page, opts| dd(page, opts)
}Auch hier gibt es wieder verschiedene Grade von Offenheit:
[:host host, :port port] # nur diese Keys
[:host host, :port port, ..] # weitere Keys erlaubt
[:host host, :port port = 80, ..] # mit Default fuer fehlenden KeyWenn Key und Binding-Name gleich heissen sollen, kannst du den Namen weglassen —
auch zusammen mit einem Default. port: = 80 bindet also port und setzt 80
ein, falls der Key fehlt:
fn conn([host: = "localhost", port: = 80] as opts) -> host <> ":" <> str(port)
conn() # => "localhost:80"
conn(host: "db") # => "db:80"
conn(port: 5432) # => "localhost:5432"
conn(host: "db", port: 5432) # => "db:5432"Das ist die kompakteste Form und spart die Wiederholung aus
[:host host = "localhost", :port port = 80]. Die Namen stehen im Koerper
direkt zur Verfuegung.
Voraussetzung ist ein bindbarer Bezeichner — ready?: bindet ready?, bei
Keys wie x-model geht die Kurzform nicht:
Parse error: Keyword ':x-model' needs an explicit variable binding in keyword
array pattern because 'x-model' is not a bindable identifierWenn ein Keyword-Key kein bindbarer Variablenname ist, braucht das Pattern ein explizites Binding:
[:x-model model, :user/id user_id, ..]Werte jeden Typs als Sub-Pattern#
An der Wert-Stelle steht nicht nur ein Binding oder ein Skalar-Literal — erlaubt ist jeder Pattern-Wert: Strings, Arrays, Maps, Sets, Tagged Values und verschachtelte Patterns mit eigenen Bindings. Der Wert wird dann strukturell gematcht, genau wie bei Map-Patterns:
fn zz([a: x, b: [1, 2]]) -> x # b muss exakt [1, 2] sein
fn box([box: %Ok[v]]) -> v # entpackt das Tagged Array
fn cfg([meta: %{k: 1}, ..]) -> :ok # Map-Wert wird mitgematcht
fn sum([pair: [x, y]]) -> x + y # Bindings im Sub-Pattern
zz([a: 5, b: [1, 2]]) # => 5
zz([a: 5, b: [1, 3]]) # kein Match (Werte verschieden)
sum([pair: [10, 20]]) # => 30Die Kurzform (key:) greift dabei nur, wenn direkt ,, ] oder = folgt;
alles andere ist ein Wert-Pattern. Defaults (key: = expr) sind wie bisher nur
zusammen mit einem einfachen Binding erlaubt, nicht mit einem Literal- oder
Sub-Pattern-Wert.
Verschachtelte Defaults tauchen auch im as-Alias auf — der Alias liefert den
effektiven Wert nach inneren Defaults:
fn zz([a: , b: %{x:, y: = 100}] as opts) -> opts
zz([a: 2, b: %{x: 10}])
# => [a: 2, b: %{ :x => 10, :y => 100 }] (y aus dem Default)Map-Patterns#
fn show(%{name: name, age: age}) -> name <> ": " <> str(age)
show(%{name: "Ada", age: 36}) # => "Ada: 36"Es gibt dabei verschiedene Stufen von Offenheit:
%{name: name} # streng
%{name: name, ..} # weitere Keys erlaubt
%{name: name, ..rest} # weitere Keys gesammelt in restWenn Key und Binding-Name identisch sind, geht auch die Kurzform:
%{name:, age:, ..}Und Bindings koennen direkt einen Default tragen, falls der Key fehlt:
%{host: host = "localhost", port: port = 80, ..}Beides laesst sich kombinieren: port: = 80 nutzt die Kurzform und setzt
einen Default. Gebunden wird wieder der Key-Name:
fn conn(%{host: = "localhost", port: = 80, ..}) -> host <> ":" <> str(port)
conn(%{}) # => "localhost:80"
conn(%{port: 5432}) # => "localhost:5432"
conn(%{host: "db", port: 5432}) # => "db:5432"Ausgeschriebene und verkuerzte Eintraege duerfen im selben Pattern stehen:
%{host: host = "localhost", port: = 80, ..}Dasselbe Prinzip gilt auch fuer Tagged Maps und fuer Keyword-Array-Patterns:
%Logger{color: = "red", label: = "x", ..}
[:host host = "localhost", :port port = 80, ..]
[host: = "localhost", port: = 80]Voraussetzung ist ein bindbarer Bezeichner. Bei Keys wie x-model oder
user/id musst du den Namen ausschreiben — die Fehlermeldung sagt das auch:
Parse error: Map pattern key 'x-model' needs an explicit variable binding
because 'x-model' is not a bindable identifierWenn ein Key kein bindbarer Variablenname ist, braucht das Pattern ein explizites Binding — die Kurzform reicht dann nicht:
%{x-model: model, user/id: user_id}Tagged Patterns#
Tagged Values lassen sich sehr präzise matchen:
fn unwrap(%Ok[value]) -> value
fn unwrap(%Err[msg]) -> throw msg
fn label(%User{name: name}) -> "User: " <> name
unwrap(%Ok[42]) # => 42
label(%User{name: "Ada"}) # => "User: Ada"Bei Tagged-Maps funktionieren dieselben Kurzformen und Defaults:
%Logger{color:, label:, sinks: sinks = [], ..}Das ist oft lesbarer als manuelle Tag- oder Type-Checks.
Tags müssen in beide Richtungen zusammenpassen. Ein getaggtes Pattern matcht nur einen Wert mit genau diesem Tag, und ein ungetaggtes Pattern matcht nur einen ungetaggten Wert. Zwei ungetaggte Seiten passen zusammen; jede Tag-Abweichung ist ein Fehlschlag. Das gilt einheitlich für Maps, Arrays und Sets.
fn strict(%User{name:, ..}) -> "nur User"
fn plain(%{name:, ..}) -> "nur ungetaggte Map"
strict(%User{name: "Ada"}) # matcht
strict(%{name: "Ada"}) # matcht NICHT — ungetaggt, aber %User erwartet
plain(%{name: "Ada"}) # matcht
plain(%User{name: "Ada"}) # matcht NICHT — getaggt, aber %{...} erwartet ungetaggtWer einen bestimmten Tag verlangt, schreibt ihn ins Pattern; wer bewusst nur ungetaggte Werte akzeptieren will, lässt ihn weg.
Tagged Scalars und Tagged Strings als Pattern#
Tagged Scalars (10min, 5EUR) und Tagged Strings (regex"...", go"hello")
sind intern getaggte Ein-Element-Werte (10min == %min[10],
go"hello" == %go["hello"]). Beide Schreibweisen sind wertgleich und daher
im Pattern austauschbar:
fn dur(10min) -> "genau zehn Minuten" # entspricht fn dur(%min[10])
fn dur(%min[n]) -> "${n} Minuten" # bindet die Zahl
dur(10min) # => "genau zehn Minuten"
dur(%min[10]) # => dasselbe (wertgleich)
dur(5min) # => "5 Minuten" (zweite Klausel bindet n)
dur(10sec) # matcht keine Klausel: anderer TagEin literaler Tagged Scalar/String matcht nur bei gleichem Tag und gleichem
Wert; 10min matcht weder 11min (anderer Wert) noch 10sec (anderer Tag)
noch das ungetaggte 10. Zum Binden statt Vergleichen nimmt man die
%Tag[binding]-Form (%min[n]).
Set-Patterns#
Sets koennen exakt oder mit offenem Rest gematcht werden:
match ids {
%<> -> "leer"
%<1, 2> -> "genau 1 und 2"
%<1, 2, ..> -> "enthaelt mindestens 1 und 2"
%<1, 2, ..rest> -> rest
%Seen<1, 2> -> "genau Seen mit 1 und 2"
_ -> "etwas anderes"
}Wichtig dabei:
- die Reihenfolge der Elemente im Pattern ist egal
%Tag<...>verlangt zusaetzlich denselben Tag%<a, b>bleibt exakt und erlaubt keine weiteren Elemente%<a, b, ..>erlaubt weitere Elemente ohne Binding%<a, b, ..rest>bindet den uebrigen Rest als ungetaggtes Set
String-Patterns#
String-Patterns zerlegen Text direkt in der Signatur, ohne erst manuell mit
split oder Substrings zu arbeiten. Der haeufigste Fall ist ein Praefix,
dessen Rest gebunden wird:
fn parse("GET " <> path) -> %{method: "GET", path: path}
fn parse("POST " <> path) -> %{method: "POST", path: path}Der gebundene Teil darf auch zwischen zwei festen Stuecken liegen. Dann muessen Anfang und Ende passen, und gebunden wird, was dazwischen steht:
fn shout("hello" <> rest <> "!") -> rest
shout("hello world!") # => " world"
shout("hello world") # kein Match — das "!" fehltRange-Patterns#
Statt viele Literale aufzuzaehlen, kannst du einen Bereich matchen:
fn size(1..9) -> "klein"
fn size(10..99) -> "mittel"
fn size(x) -> "gross"
size(5) # => "klein"
size(42) # => "mittel"
size(500) # => "gross"Es gelten dieselben Formen wie bei Range-Werten:
| Form | Bedeutung |
|---|---|
1..9 | inklusive Untergrenze und Obergrenze |
1..=9 | explizit inklusiv, gleichbedeutend |
1..<9 | Obergrenze ausgeschlossen |
10..* | offen nach oben |
*..9, *..<10 | offen nach unten (nur als Pattern) |
0..100 by 10 | nur jeder zehnte Wert |
'a'..'c' | Zeichenbereich (Einzelzeichen-Strings) |
Die Grenzen duerfen Ausdruecke sein und sehen den umgebenden Scope:
my max = 10
fn f(1..max) -> "drin"
fn f(x) -> "draussen"Ein Range-Pattern bedeutet exakt dasselbe wie der in-Operator — es ist
dieselbe Pruefung, nur in Pattern-Position:
match v { 1..10 -> "drin" } # gleiche Antwort wie
v in 1..10Daraus folgt auch das Verhalten in den Randfaellen: Werte, die keine Zahl sind
(oder bei Zeichenbereichen kein einzelnes Zeichen), matchen nicht, und bei
einem Schritt ungleich 1 muss der Wert auch auf dem Raster liegen. 1.5 liegt
deshalb nicht in 1..10. Nur *..9 hat keinen Start und damit kein Raster:
es trifft jede Zahl bis zur Grenze, also auch 8.5.
Damit wird eine Stufentabelle ohne Guards lesbar:
fn grade(p) -> match p {
90..* -> 1
75..<90 -> 2
60..<75 -> 3
50..<60 -> 4
*..<50 -> 5
}Ein Funktionsaufruf ist kein Pattern: range(0, 10, 2) -> ... ist ein
Parse-Fehler (er wuerde sonst still nie treffen). Dafuer gibt es by oder
einen Guard (x when x in range(0, 10, 2) -> ...).
Aliase mit as#
Manchmal brauchst du die Teile und das Ganze. as name bindet zusaetzlich
den kompletten gematchten Wert:
fn f([a, b] as arr) -> str(a + b) <> " aus " <> str(arr)
f([1, 2]) # => "3 aus [1, 2]"Das funktioniert nicht nur bei Keyword-Array-Patterns, wo es weiter oben schon vorkam, sondern bei Array-, Map- und Tagged-Patterns:
fn label(%User{name: n, ..} as user) -> n <> " hat " <> str(len(user)) <> " Felder"Ausserdem darf as direkt am Wert eines Map-Pattern-Eintrags stehen. So
bekommst du die destrukturierten Teile und den Eintrag als Ganzes:
fn f(%{
name:,
address: %{street:, ..} as address, # bindet street UND address
..
}) -> street <> " (" <> str(address) <> ")"
fn g(%{u: %User{name: n} as inner, ..}) -> n # auch mit Tagged-Patterns
val %{a: [x, y] as pair, ..} = %{a: [1, 2], b: 3}
val %{status: "ok" as s, ..} = response # Literal pruefen UND bindenErlaubt ist der Eintrags-Alias an allem, was selbst kein einfaches Binding ist: verschachtelte Map-, Array-, Set- und Tagged-Patterns sowie Literale. Schlaegt das innere Pattern fehl, matcht wie ueblich der ganze Eintrag nicht — der Alias wird nur bei Erfolg gebunden. Zwei Formen sind bewusst Parse-Fehler:
%{a: n as x} # redundant — `a: x` bindet den Wert bereits direkt
%{a: b as mut c} # 'as mut' gibt es nur fuer ganze ParameterAussenherum hat as weiterhin enge Grenzen — es steht nur an einem
Parameter-Pattern oder eben am Wert eines Map-Eintrags. Diese Formen sind
Parse-Fehler:
my [a, b] as pair = [1, 2] # nicht aussen an my/val/const-Bindungen
match u { %User{name: n} as who -> n } # nicht aussen an match-ArmenIn Kombination mit Oder-Patterns ist as dagegen erlaubt und bindet den
gesamten Wert — siehe
Kapitel 08.
Verschachtelung und ihre Grenzen#
Patterns lassen sich ineinander schachteln:
fn f([[a, b], [c, d]]) -> a + b + c + d # Array in Array
fn g(%{user: %{name: n, ..}, ..}) -> n # Map in Map
fn h([%{id: id, ..}, ..]) -> id # Map in Array
fn k(%Ok[[first, ..]]) -> first # Array in Tagged ValueDas gilt in beide Richtungen: Auch als Wert innerhalb eines Map-Patterns darf ein Array- oder Tagged-Pattern stehen.
fn f(%{b: [x, y], ..}) -> x + y
fn g(%A{c: %Ok[v], ..}) -> v
fn h(%{items: [first, ..rest], ..}) -> str(first) <> "/" <> str(len(rest))
f(%{b: [3, 4]}) # => 7
g(%A{c: %Ok[42]}) # => 42
h(%{items: [1, 2, 3]}) # => "1/2"Passt die Form nicht — falsche Laenge, falscher Typ, falscher Tag —, ist das wie ueberall sonst einfach kein Match und faellt auf die naechste Klausel:
fn f(%{b: [x, y], ..}) -> "zwei:" <> str(x + y)
fn f(x) -> "kein match"
f(%{b: [1, 2, 3]}) # => "kein match"
f(%{b: "kein array"}) # => "kein match"Alternativ kannst du die Ebene binden und in einem zweiten Schritt matchen — sinnvoll, wenn du mehrere Formen unterscheiden willst:
fn f(%{b: inner, ..}) -> match inner {
[x, y] -> x + y
_ -> 0
}Alternativen mit Oder-Patterns#
Mehrere Alternativen können sich einen Arm teilen:
match x {
0 | 1 | 2 -> "small"
:ok | :success -> "positive"
%Ok[v] | %Err[v] -> "got $v"
_ -> "other"
}Bindings muessen in allen Alternativen denselben Namen haben.
Dieselbe Form geht auch in Funktionsparametern — fn small(1 | 2 | 3) -> ....
Details dazu in
Kapitel 08.
Wo Patterns vorkommen#
Guards#
Manchmal reicht die Form allein nicht. Dann kommt mit when eine Bedingung
dazu, die zwischen Signatur und -> steht:
fn classify(n) when n < 0 -> "negativ"
fn classify(0) -> "null"
fn classify(n) -> "positiv"Lesart:
- das Pattern passt grundsätzlich
- zusätzlich muss die Bedingung wahr sein
Guards gelten fuer fn- und gen fn-Klauseln, match-Arme und catch-Arme.
Die Bedingung ist ein normaler Ausdruck; die Verknuepfung schreibst du mit
&& und || — die Wortformen and/or gibt es in Antra nicht.
Wie Klauseln ausgewaehlt werden, was bei fehlendem Treffer passiert und welche Fallstricke es bei der Reihenfolge gibt, steht ausfuehrlich in Kapitel 08.
match als Ausdruck#
Pattern Matching ist nicht nur für Funktionen da:
my text = match result {
%Ok[value] -> "ok: " <> str(value)
%Err[msg] -> "fehler: " <> msg
}
match %Ok[42] {
%Ok[value] -> "ok: " <> str(value)
%Err[msg] -> "fehler: " <> msg
} # => "ok: 42"Das ist oft die sauberste Art, mehrere Formen eines Werts zu behandeln.
Ein Arm-Body muss dabei kein Ausdruck sein: ein einzelnes Statement ist
ebenfalls erlaubt, ohne { } drumherum. Das betrifft vorzeitiges Zurückgeben,
Schleifensteuerung und Zuweisungen:
match req {
"GET " <> path -> return "got:$path"
_ -> "?"
}
while (streaming) {
match load(id) {
%Ok[board] -> last if board.done
%Err[_] -> streaming = false
}
}Mehrere Anweisungen brauchen weiterhin einen Block:
%Ok[board] -> {
last_sent = board.version
seen += 1
}Eine Zuweisung als Arm-Body liefert dabei den zugewiesenen Wert, der Arm ist also weiter ein Ausdruck:
my x = 1
println(match 1 { 1 -> x = 5 _ -> 0 }) # => 5Zweiweg-Kurzform mit if#
Fuer den haeufigen Zweiweg-Fall gibt es eine lokale Kurzform mit if pattern <- expr:
if %Ok[user] <- find_user(id) {
println(user.name)
} else %Err[err] {
println(err.message)
} else {
println("unexpected")
}
if %Ok[user] <- find_user(1) {
user.name
} else %Err[err] {
err.message
} else {
"unexpected"
} # => "Ada"Das ist praktisch, wenn du lokal genau einen Erfolgs- und hoechstens einen
zweiten Pattern-Fall plus optionalen Catch-all brauchst. Sobald mehr sichtbare
Faelle im Spiel sind, bleibt match die klarere Form.
Schleifenform mit while#
Dieselbe Idee gibt es als Schleife. while pattern <- expr laeuft, solange das
Pattern matcht — gedacht fuer pattern-getriebene Reads und Iteratoren:
while %Some[line] <- File.read_line(fh).! {
println(line)
}
while %Some[line] <- File.read_line(fh).! {
line
} # => alle Zeilen nacheinanderWenn der erste Nicht-Match nicht nur die Schleife beenden, sondern noch
ausgewertet werden soll, gibt es dafuer zusaetzlich eine
else pattern-Form:
while %Ok[%Some[line]] <- File.read_line(fh) {
println(line)
} else %Ok[%None[]] {
println("eof")
} else %Err[err] {
println(err.message)
}
while %Ok[%Some[line]] <- File.read_line(fh) {
line
} else %Ok[%None[]] {
"eof"
} else %Err[err] {
err.message
} # => "eof" am EndeDamit laeuft der Body solange das erste Pattern matcht; beim ersten Nicht-Match
wird derselbe Wert genau einmal gegen die else pattern-Branches
geprueft.
Regex-Branches#
Die Regex-Branch-Form ist streng genommen kein Pattern, gehoert beim Lesen aber hierher: Sie oeffnet im genommenen Branch ebenfalls neue lokale Bindings.
if "2026-05-19" =~ regex"(?<year>\d{4})-(\d{2})-(\d{2})" {
println(year)
println(_1)
println(_2)
println(_3)
}
if "2026-05-19" =~ regex"(?<year>\d{4})-(\d{2})-(\d{2})" {
year
} # => "2026"
unless "world" =~ regex"(?<prefix>^wor)(ld)" {
println("fallback")
} else {
println(prefix)
println(_1)
println(_2)
}
unless "world" =~ regex"(?<prefix>^wor)(ld)" {
"fallback"
} else {
prefix
} # => "wor"Die Lesart dazu ist:
- bei
if text =~ rx { ... }sind benannte und nummerierte Captures nur im erfolgreichen Branch sichtbar - bei
unless text =~ rx { ... } else ...liegen dieselben Captures nur im ausgefuehrtenelse-Branch - optionale, nicht belegte Gruppen bleiben positionsstabil als
undefgebunden - ausserhalb des genommenen Branches existieren diese Namen nicht
Destrukturierende Bindungen#
Auch my, val und const nehmen ein Pattern statt nur eines Namens — das
ist das Destructuring vom Kapitelanfang:
val %{a: x} = %{a: 1}
const [p, q] = [7, 8]
my [:host h, :port prt] = [host: "localhost", port: 80]
println(str(x) <> "/" <> str(p) <> "/" <> h) # => "1/7/localhost"Zur Erinnerung: Anders als bei match oder if pattern <- gibt es hier
keinen zweiten Zweig — ein Mismatch ist ein Laufzeitfehler. as ist in dieser
Position nicht erlaubt.
Patterns in for-Schleifen#
for bindet links von <- ebenfalls ein volles Pattern, nicht nur einen
Namen:
for [a, b] <- [[1, 2], [3, 4]] {
println(a + b) # => 3, dann 7
}
for %{n: n, ..} <- [%{n: 1}, %{n: 2}] {
println(n) # => 1, dann 2
}Auch hier ist ein Nicht-Match ein Fehler, kein stilles Ueberspringen — ein Element der falschen Form bricht die Schleife ab, nachdem die passenden Elemente davor schon abgearbeitet wurden. Die Meldung nennt das schuldige Element:
Error: 'for' pattern does not match this element: [3]
Hint: the pattern left of '<-' must match every element of the sequence.
Hint: if elements can have different shapes, bind the element and use 'match' in the body.Wenn Elemente unterschiedlicher Form vorkommen koennen, ist genau das der
Ausweg: das Element binden und im Koerper mit match unterscheiden.
Mehr zu den Schleifenformen steht in Kapitel 09.
Was bei einem Fehlschlag passiert#
Ob ein Nicht-Match harmlos ist, haengt vom Kontext ab. Das ist die wichtigste Tabelle des Kapitels:
| Kontext | Bei Nicht-Match |
|---|---|
fn-Klausel | naechste Klausel; passt keine, Laufzeitfehler |
match-Arm | naechster Arm; passt keiner, Laufzeitfehler |
if pattern <- expr | kein Fehler — der Branch wird nur nicht genommen |
while pattern <- expr | kein Fehler — die Schleife endet |
my / val / const | Laufzeitfehler |
for pattern <- ... | Laufzeitfehler |
Die Fehlermeldungen benennen den Fall jeweils konkret:
Error: no matching clause in match expression
Error: No matching clause for f with 1 args: 9
Error: Array destructuring expects exactly 2 element(s), got 1Ein Catch-all-Arm (_) beziehungsweise eine Klausel ohne Pattern ist deshalb
oft die einfachste Absicherung.
Verstehen und anwenden#
Warum ist das hilfreich?#
Pattern Matching reduziert typischen Boilerplate:
Statt so zu denken:
- prüfe erst den Typ oder Tag
- hole dann einzelne Felder heraus
- behandle Sonderfälle später
kannst du so denken:
- nimm gleich nur die Form an, die du hier brauchst
Das macht viele Funktionen kleiner und spezifischer.
Mentales Modell für Umsteiger#
Wenn du Pattern Matching noch nie benutzt hast, hilft diese Übersetzung:
- Array-Pattern ist wie "entpacken plus prüfen"
- Map-Pattern ist wie "Felder erwarten plus binden"
- Tagged Pattern ist wie "Form prüfen plus Inhalt entpacken"
Gute erste Regeln#
Für die Praxis reichen am Anfang diese vier Regeln:
- Ein Pattern beschreibt die Form eines Werts.
- Namen im Pattern binden Teilwerte.
_ignoriert Teilwerte.- Mehrere
fn-Klauseln odermatch-Arme beschreiben mehrere erlaubte Formen.
Fuer if pattern <- expr gilt zusaetzlich:
- Nicht-Match ist dort kein Fehler, sondern einfach ein nicht genommener Branch.
Ein komplexeres Beispiel zum Abschluss#
Zum Schluss ein Beispiel, das die Formen dieses Kapitels kombiniert: ein
Tagged-Map-Pattern mit Shorthand-Keys, eine verschachtelte offene Map, ein
Array-Pattern mit Rest-Platzhalter, ein as-Alias auf den Gesamtwert, ein
when-Guard — und eine zweite Klausel als Auffangnetz:
fn describe(%Person{
name:, # Shorthand: bindet name
age:,
address: %{
city:, # nur city interessiert -
.. # weitere Adressfelder sind ok
},
hobbies: [first_hobby, .._], # mindestens ein Hobby, Rest egal
.. # weitere Person-Felder sind ok
} as p) # p bindet zusaetzlich die GANZE Person
when age >= 18 -> { # Guard: laeuft nach dem Form-Match,
# sieht alle Pattern-Bindungen (age, name, ...)
println(name <> " (" <> str(age) <> ") aus " <> city <> ", mag " <> first_hobby)
p
}
fn describe(%Person{ name:, .. }) ->
println(name <> " wird nicht im Detail angezeigt")
describe(%Person{
name: "Ada",
age: 36,
address: %{
city: "Berlin",
street: "Elligersweg",
house_number: 27
},
hobbies: ["Schach", "Klettern"],
newsletter: true
})
# => Ada (36) aus Berlin, mag Schach
describe(%Person{ name: "Kind", age: 6, address: %{ city: "Bonn" }, hobbies: [] })
# => Kind wird nicht im Detail angezeigtZum when age >= 18: Das ist ein Guard (siehe Guards) — er
laeuft erst, nachdem das Pattern strukturell gepasst hat, und sieht dabei
alle Bindungen, die das Pattern gerade erzeugt hat (age, aber genauso
name, city oder p). Ein Guard kann Bindungen auch kombinieren, etwa
when age >= 18 && len(first_hobby) > 0. Liefert er einen falsy Wert, gilt
die ganze Klausel als nicht getroffen — exakt so, als haette schon die Form
nicht gepasst.
Der zweite Aufruf landet deshalb gleich aus zwei Gruenden in der zweiten
Klausel: der Guard age >= 18 schlaegt fehl und hobbies: [] passt nicht
auf [first_hobby, .._]. Beide Wege — Form-Mismatch und Guard-Fehlschlag —
muenden in dasselbe kontrollierte Durchfallen zur naechsten Klausel. Genau das
ist die Staerke der Klausel-Auswahl: die erste Klausel beschreibt praezise den
Fall, den sie behandelt (richtige Form und richtige Bedingung), und alles
andere faellt weiter, statt irgendwo in einem if-Gestruepp zu landen.
