Struktur und Typen
Protokolle
Ein Protokoll beschreibt eine Fähigkeit, die viele Typen haben können: sich anzeigen, addieren, vergleichen, indexieren, aufrufen. Ein Typ bekommt die Fähigkeit, indem er das Protokoll implementiert. Danach funktionieren die passenden Operatoren und Funktionen für seine Werte, ohne dass der aufrufende Code wissen muss, welcher Typ dahintersteckt.
Die Idee kennt man aus Clojure (Protokolle), Rust (Traits) oder Python (Dunder-Methoden). In Antra sind es kleine benannte Gruppen von Methoden, die nach dem Wert dispatchen, auf dem sie aufgerufen werden.
Ein erstes Beispiel#
protocol Shape {
fn __area__(self)
fn __describe__(self) -> "Fläche " <> str(__area__(self))
}
impl Shape for %Circle{..} {
fn __area__(%Circle{r:}) -> 3 * r * r
}
impl Shape for %Rect[..] {
fn __area__(%Rect[w, h]) -> w * h
fn __describe__(_) -> "ein Rechteck"
}
println(__area__(%Circle{r: 2})) # 12
println(__describe__(%Circle{r: 1})) # Fläche 3
println(__describe__(%Rect[2, 3])) # ein Rechteck
println(map([%Circle{r: 1}, %Rect[1, 2]], &__area__)) # [3, 2]Das Protokoll Shape verlangt __area__ und bringt für __describe__ einen
Default mit. %Circle{..} liefert nur __area__ und erbt den Default,
%Rect[..] überschreibt beide. __area__(x) ruft die Methode, die zum Typ
von x gehört.
Protokoll, impl und Methodennamen#
protocol Name { … }deklariert die Methoden. Eine Methode ohne Rumpf (fn __area__(self)) ist abstrakt und muss von jedemimplgeliefert werden, eine mit Rumpf ist ein Default. Fehlt eine abstrakte Methode, meldet Antra das schon bei der Definition desimpl, nicht erst beim ersten Aufruf.impl Name for Ziel { … }liefert die Methoden für einen Typ.- Methodennamen sind Dunder-Namen (
__area__): Unter diesem Namen ist die Methode überall als Funktion aufrufbar und dispatcht über ihr erstes Argument. Ein Name ohne Unterstriche (area) ist nur innerhalb derimpl-Methoden sichtbar. - Dispatch über das erste Argument:
__area__(x)sucht dasimplfür den Typ vonx. Fehlt eins, ist das ein klarer Fehler (No implementation of __area__ for type '%Tri[]'). - Innerhalb eines
implsind die anderen Methoden desselben Protokolls direkt aufrufbar, wie__area__(self)im Default oben.
Worauf ein impl zielt#
| Ziel | Beispiel | Werte |
|---|---|---|
| Getaggtes Array | impl P for %Rect[..] | %Rect[2, 3] |
| Getaggte Map | impl P for %Circle{..} | %Circle{r: 1}, auch ein bless-Objekt des Packages Circle |
| Getaggtes Set | impl P for %Tags<..> | %Tags<:a, :b> |
| Builtin-Typ | impl P for Number | 5, 1.5 (auch String, Array, Map, Set, Range, Generator, Bool) |
| Opaker Typ | impl P for FileHandle | offene Dateien (auch ByteString, Socket, Process, Blob) |
| Getaggte Map mit Feldtypen | impl P for %Vec{x :- Int, y :- Int} | %Vec{x: 1, y: 2}; --check kennt die Felder |
typedef | impl P for Result | jedes Mitglied der Union, hier %Ok[..] und %Err[..] |
typedef eines Records | impl P for Vec mit typedef Vec = %Vec{x :- Int, y :- Int} | %Vec{x: 1, y: 2}; --check kennt die Felder |
%Tag[..], %Tag{..} und %Tag<..> sind drei verschiedene Ziele. Ein
Package-Name (impl P for Wallet) ist kein Ziel; für Package-Objekte nimmt man
%Wallet{..} oder package Wallet with P.
Nennt das Ziel seine Felder, gelten sie für die Operanden der Methoden:
import Antra::Ops.{Add}
typedef Vec = %Vec{x :- Int, y :- Int}
impl Add for Vec {
fn __add__(a, b) -> %Vec{x: a.x + b.x, y: a.y + b.y}
}
println(%Vec{x: 1, y: 2} + %Vec{x: 3, y: 4}) # %Vec{ :x => 4, :y => 6 }Hier sind a und b beide Vec (__add__ aus Antra::Ops erwartet zweimal
Self), und ein Tippfehler wie a.z ist ein --check-Fehler. Mit %Vec{..}
als Ziel ist nur bekannt, dass es eine %Vec-Map ist.
Das Protokoll muss sichtbar sein: deklariert in der Datei oder importiert
(import Antra::Ops.{Add}, import Antra::Ops.*). Ohne Import ist impl Add for … ein Fehler, in --check wie zur Laufzeit.
Defaults, Klauseln und Guards#
Eine Methode darf mehrere Klauseln haben. Sie werden der Reihe nach gegen die
Argumente gematcht, mit Patterns und when-Guards wie bei einer fn:
protocol Size {
fn __size__(self)
}
impl Size for %Box[..] {
fn __size__(%Box[]) -> :empty
fn __size__(%Box[..xs]) when len(xs) > 2 -> :big
fn __size__(_) -> :small
}
println([__size__(%Box[]), __size__(%Box[1]), __size__(%Box[1, 2, 3])]) # [:empty, :small, :big]Ein Default im Protokoll kann die abstrakten Methoden benutzen und gilt für
jedes impl, das ihn nicht überschreibt.
package P with Proto#
Ein Package kann Protokolle direkt im Header nennen. Die Protokoll-Methoden
stehen dann im Package-Body, das Ziel ist %P{..}:
import Antra::Ops.*
package Money with Add, Eq, Ord {
pub field cents = 0
pub fn new(c) -> bless(%{cents: c}, Money)
fn __add__(a, b) -> Money.new(a.cents + b.cents)
fn __eq__(a, b) -> a.cents == b.cents
fn __cmp__(a, b) -> a.cents <=> b.cents
}
val m = Money.new(5) + Money.new(7)
println([m.cents, m == Money.new(12), m > Money.new(3)]) # [12, true, true]Wie bei einem impl prüft Antra schon bei der Package-Definition, ob jede
abstrakte Methode da ist. Mehr zu Packages,
bless und Feldern steht in
Kapitel 14.
Operatoren#
Hat der linke Operand einen Tag (%T[..], %T{..}, %T<..>), sucht der
Operator die passende Methode für dessen Typ:
| Operator | Methode |
|---|---|
a + b, a - b, a * b, a / b, a % b | __add__, __sub__, __mul__, __div__, __mod__ |
-a | __neg__(a) |
a == b, a != b | __eq__ (!= ist das Gegenteil); ohne Impl Vergleich der Inhalte |
a < b, a <= b | __lt__ bzw. __le__, sonst aus __cmp__ |
a > b, a >= b, a <=> b | __cmp__ (liefert :lt, :eq oder :gt) |
__cmp__ reicht für alle Vergleiche, und Array.sort, Array.min und
Array.max ordnen getaggte Werte ebenfalls darüber:
import Antra::Array
protocol Ord {
fn __cmp__(a, b)
}
impl Ord for %Version[..] {
fn __cmp__(%Version[a, b], %Version[c, d]) -> if a == c then b <=> d else a <=> c
}
println(%Version[1, 2] > %Version[1, 0]) # true
println(Array.sort([%Version[2, 0], %Version[1, 5]])) # [%Version[1, 5], %Version[2, 0]]Fehlt die Methode, ist das ein Fehler (No implementation of __cmp__ for tag 'Version'), nur ==/!= vergleichen dann die Inhalte. Ungetaggte Werte
(Zahlen, Strings, Arrays) rechnen wie immer.
Zugriff: x[k], in, len#
| Ausdruck | Methode | Ergebnis |
|---|---|---|
x[k], x?[k] | __get__(x, k) | der Wert |
v in x | __contains__(x, v) | true oder false |
len(x) | __count__(x) | %Some[n], oder %None[] für die Länge der Daten |
Innerhalb der Methode greift derselbe Zugriff auf denselben Wert direkt auf die Daten zu. Ein Array mit Index ab 1 ist deshalb ein Einzeiler:
protocol Access {
fn __get__(self, k)
fn __contains__(self, v)
fn __count__(self)
}
impl Access for %Vec[..] {
fn __get__(self, k) -> self[k - 1]
fn __contains__(self, v) -> v in [..self]
fn __count__(self) -> %Some[len(self)]
}
val v = %Vec[10, 20, 30]
println([v[1], 20 in v, len(v)]) # [10, true, 3]Bereiche und Slices (x[1..2], x[0:2]) lesen immer die Daten. __count__
ist dieselbe Methode wie in Antra::Enumerable und liefert deshalb ein
Option.
Sets und Aufrufe: __hash__ und __call__#
__hash__(x) bestimmt, wann zwei getaggte Werte in einem Set dasselbe Element
sind. __call__(x, …) macht einen Wert aufrufbar, auch als Callback:
protocol Key {
fn __hash__(self)
}
impl Key for %User{..} {
fn __hash__(self) -> self.id
}
protocol Callable {
fn __call__(self, x)
}
impl Callable for %Adder{..} {
fn __call__(self, x) -> x + self.n
}
val users = %<%User{id: 1, name: "Ada"}, %User{id: 1, name: "Ada L."}>
println([len(users), %User{id: 1} in users]) # [1, true]
val add2 = %Adder{n: 2}
println([add2(40), map([1, 2], add2)]) # [42, [3, 4]]__hash__ gilt für Set-Literale, in und Set.*, aber nur für das Element
selbst; Map-Schlüssel bleiben Text. In __call__ ist self(…) nicht
aufrufbar, damit es keine Endlosschleife gibt.
Anzeige: __str__#
str(...), print/println und String-Interpolation suchen __str__. Ohne
Impl zeigen sie die Struktur (%Wallet{ :cents => 950 }):
import Antra::Display.*
impl Display for %Euro[..] {
fn __str__(%Euro[c]) -> str(c / 100) <> " EUR"
}
println(%Euro[950]) # 9.5 EUR
println([%Euro[100]]) # [1 EUR]dd(...) und Fehlermeldungen nutzen __str__ bewusst nicht; sie zeigen, was
wirklich im Wert steht.
Eigene Sequenzen: impl Enumerable#
Arrays, Ranges, Strings, Maps, Sets und Generatoren durchlaufen for, die
Built-ins map/filter und alle Funktionen aus Antra::Enum von sich aus.
Ein eigener Typ kommt mit einer Methode dazu: __reduce__ aus
Antra::Enumerable.
import Antra::Enumerable.*
import Antra::Enum
import Antra::Array
# ein Stapel: das zuletzt gelegte Element liegt oben und kommt zuerst
impl Enumerable for %Stack[..] {
fn __reduce__(self, acc, f) -> Enum.reduce(Array.reverse([..self]), acc, f)
fn __count__(self) -> %Some[len([..self])]
}
val s = %Stack[1, 2, 3]
println(Enum.map(s, \_ * 10)) # [30, 20, 10]
println(Enum.find(s, \_ < 3)) # %Some[2]
println(Enum.count(s)) # 3
println(map(s, |x, i| -> "${i}:${x}")) # ['0:3', '1:2', '2:1']
for x <- s { print(x, " ") } # 3 2 1
println()
println(implements?(s, Enumerable)) # true__reduce__(self, acc, f) ist die Pflicht: Sie ruft f(acc, x) für jedes
Element in der gewünschten Reihenfolge und gibt den letzten acc zurück. Am
einfachsten reicht sie an ein Array weiter, wie oben ([..self] sind die Daten
des getaggten Werts; ein len(self) oder Enum.*(self) in der Methode würde
sich selbst aufrufen). Damit funktioniert alles andere: map, filter,
find, reduce, zip, chunk, for, die Position im Callback (|x, i|,
der Index) und implements?.
Drei weitere Methoden sind optional und nur Abkürzungen, die Enum nutzt,
wenn sie da sind; jede liefert %Some[…] oder %None[] (dann wird gezählt
bzw. durchlaufen):
| Methode | wofür | Ergebnis |
|---|---|---|
__count__(self) | Enum.count, Enum.empty?, Enum.drop | %Some[n] |
__member__(self, v) | Enum.member? | %Some[true] / %Some[false] |
__slice__(self, start, n) | Enum.take, Enum.drop | %Some[array] |
Ohne __count__ zählt Enum.count die Elemente über __reduce__ ab. Für
len(x) gilt unabhängig davon __count__ aus dem Zugriff (siehe oben).
Der statische Checker weiß das auch: ein Wert mit impl Enumerable passt auf
Seq[T], also auf jede Funktion aus Antra::Enum; ein getaggter Wert ohne
impl ist dort ein Typfehler.
Eigene Literale: impl Refine#
Ein getaggtes Literal wie regex"\d+" ist zuerst ein getaggter Wert: ein
String-Literal name"…" wird zu %name["…"], %Tag{…} und %Tag<…> bleiben,
was sie sind. Gibt es für diese Form ein impl Refine (aus Antra::Refine),
ruft der Interpreter beim Erzeugen __refine__(self) und nimmt dessen
Ergebnis. So wird aus regex"…" ein kompilierter Regex, und so baut man eigene
Literale, die schon beim Schreiben geprüft werden:
import Antra::Refine.*
# email"…": beim Erzeugen geprüft, danach ein %Email{…}
impl Refine for %email[..] {
fn __refine__(%email[text]) -> {
val parts = split(text, "@")
throw "not an email address: ${text}" unless len(parts) == 2 && parts[0] != "" && parts[1] != ""
%Email{user: parts[0], domain: parts[1]}
}
}
val e = email"ada@example.org"
println(e.domain) # example.org
val name = "grace"
println(email"${name}@navy.mil") # %Email{ :domain => 'navy.mil', :user => 'grace' }
println <| try {
email"kaputt"
} catch {
err -> str(err)
} # not an email address: kaputt- Die drei String-Formen gelten wie überall:
email"…"interpoliert vor__refine__,email'…'bleibt roh,email`…`interpoliert ohne Escapes. - Ziel des
implist die getaggte Form:%email[..]füremail"…",%Sum{..}für%Sum{a: 3, b: 4},%Marker<..>für%Marker<1, 2>. __refine__darf alles liefern: einen anderen getaggten Wert, eine Zahl, einen String, auch dieselbe Form. Ein Literal derselben Form innerhalb von__refine__wird nicht noch einmal verfeinert, so kann eine Methode den Wert ergänzen (%Pt{x:, y:}→%Pt{x: x, y: y, len: x + y}).- Ein getaggtes Array-Literal
%tag[…]wird nicht verfeinert, nur die String-Formtag"…";%Some[…]& Co. bleiben so einfache Werte. regex,jsundcssfunktionieren ohne Import über einen eingebauten Weg;Antra::Refine::Regexzeigt dieselbe Umsetzung alsimpl Refineund taugt als Vorlage.
Fertige Protokolle der Standardbibliothek#
| Paket | Protokolle | Wofür |
|---|---|---|
Antra::Ops | Add, Sub, Mul, Div, Mod, Neg, Eq, Ord, Index, Contains, Hash | je ein Operator oder Zugriff |
Antra::Display | Display | __str__ |
Antra::Enumerable | Enumerable | eigene Sequenzen für Antra::Enum (__reduce__, __count__, __member__, __slice__) |
| Prelude | Unwrap, Propagate, UnwrapOrThrow | .?, .^ und .! für eigene Ergebnis-Typen (Kapitel 11) |
Antra::Refine | Refine | eigene getaggte Literale (__refine__, siehe oben) |
Antra::Duration | Duration | Rechnen und Vergleichen von 10sec, 2min usw. über eine gemeinsame Basis (__base__, __add__, __eq__, …; Stdlib-Referenz) |
Antra::Ops hat bewusst kein Sammel-Protokoll wie Num: Ein impl muss jede
abstrakte Methode liefern, und ein 2D-Vektor soll addieren können, ohne % zu
erfinden. Ein Typ nimmt sich mit package Vec with Add, Mul, Eq genau die, die
er kann. Nötig sind die fertigen Protokolle nicht: Die Operatoren schauen nur
nach dem Methodennamen, ein eigenes Protokoll mit __add__ wirkt genauso.
Typen an Protokollen#
Protokoll- und impl-Methoden dürfen :--Annotationen tragen. Die der
Deklaration gelten für jedes impl, das selbst keine schreibt, und Self
steht für den implementierenden Typ:
protocol Semigroup {
fn __add__(a :- Self, b :- Self) :- Self
}
impl Semigroup for %V[..] {
fn __add__(%V[a, b], %V[c, d]) -> %V[a + c, b + d]
}
val v :- %V[Number, Number] = %V[1, 2] + %V[3, 4]
println(v) # %V[4, 6]--check prüft dann den Rumpf, die Operanden (%V[1, 2] + 5 ist ein Fehler)
und das Ergebnis. In den Methoden eines Packages steht Self für das Package
selbst (pub fn moved(self :- Self) :- Self); ein impl mit eigenen, abweichenden Annotationen meldet
impl-signature-mismatch. --check-types oder @types(runtime) vor dem impl
prüfen dasselbe zur Laufzeit. Die Einzelheiten stehen in
Kapitel 16.
Fehler und ihre Bedeutung#
| Meldung | Bedeutung |
|---|---|
No implementation of __area__ for type '%Tri[]' | Für diesen Typ gibt es kein impl (oder das Paket ist nicht importiert) |
impl Shape for %Tri[..] does not satisfy protocol 'Shape': missing method '__area__' | Ein impl liefert eine abstrakte Methode nicht (bei der Definition) |
package 'Money' does not satisfy protocol 'Ord': missing method '__cmp__' | Ein package … with liefert eine abstrakte Methode nicht |
Conflicting implementations for __count__ on %Bag{..} … | Zwei Protokolle definieren dieselbe Methode für denselben Typ |
Duplicate impl P for %Ok[] | Dasselbe Protokoll ist für diesen Typ zweimal implementiert |
Unknown impl target 'Wallet' | Das Ziel ist kein Typ (ein Package-Name statt %Wallet{..}) |
impl Add for %V[..]: unknown protocol 'Add' | Das Protokoll ist weder deklariert noch importiert (import Antra::Ops.{Add}) |
No matching clause for protocol method '__size__' … | Keine Klausel der Methode passt zu den Argumenten |
__cmp__ must return :lt, :eq or :gt | __cmp__ hat etwas anderes geliefert |
__count__ for %V[] must return %Some[n] or %None[] | __count__ hat etwas anderes geliefert |
Kurzreferenz#
| Methode | Wirkt auf |
|---|---|
__add__, __sub__, __mul__, __div__, __mod__ | + - * / % |
__neg__ | -a |
__eq__ | ==, != |
__lt__, __le__, __cmp__ | < <= > >= <=>, Array.sort/min/max |
__get__ | x[k], x?[k] |
__contains__ | v in x |
__count__ | len(x), Enum.count |
__hash__ | Gleichheit in Sets |
__call__ | x(…), Callbacks |
__str__ | str, print, Interpolation |
__unwrap__, __propagate__, __unwrap_bang__ | .?, .^, .! |
