Als ich mir Gedanken darüber machte, eine Homepage zu erstellen, habe ich
ein wenig
über PHP gelesen und habe entschieden, dass ich diese Sprache nicht lernen, sondern stattdessen meine
eigene Sprache erfinden will. Sprachen erfinden macht mir sowieso Spaß.
Im Folgenden nun einige Merkmale von BHP
.
BHP unterstützt eine Mischung aus funktionaler und imperativer Programmierung. Etwas Objektorientierung ist auch dabei. BHP hat syntaktische Ähnlichkeit mit C++/Java. Weitere Anleihen wurden bei Python, Scala, HTML und Haskell gemacht.
BHP hat einen eingebauten Mehrzweck-Datentyp Dom
. Ein Dom
-Objekt besteht im Wesentlichen aus einem Tag (ein String), einer
Kollektion von Attributen (Eine Zuordnung von Strings zu Objekten) und einer Liste von Elementen (irgendwelche Objekte).
Es gibt zwei syntaktische Varianten, um ein Dom
-Objekt zu erzeugen. Die erste Variante sieht zum Beispiel so aus:
def domObject = [:div, .class="someClass", .attr=3+2, "Hallo", [:br], "Welt"]
Hier wird ein Dom
-Objekt mit dem Tag div
erzeugt, das zwei Attribute hat (nämlich
mit dem Wert class
"someClass"
und
mit dem Wert attr
5
) und drei Elemente mit den Werten "Hallo"
,
[:br]
(ein Dom
-Objekt ohne Attribute oder Elemente) und "Welt"
umfasst.
In der zweiten Variante, die stark an XML/HTML angelehnt ist, würde man dasselbe Objekt so definieren:
def domObject = <div class="someClass" attr=(2+3)>Hallo<br/>Welt</div>
Die Werte der Attribute sind hierbei durch BHP-Ausdrücke gegeben. Das, was zwischen den Tags steht, wird wie der Inhalt eines Stringliterals behandelt.
Wenn ein Dom
-Objekt, das ein von null
verschiedenes Tag besitzt, in einen String umgewandelt wird, entsteht HTML-/XML-Code.
Aus dem Obigen entsteht so der String:
"<div class="someClass" attr="5">Hallo<br/>Welt</div>"
Will man Inhalte von Variablen oder berechnete Daten in die Elementliste aufnehmen, so hat man mehrere Möglichkeiten
(Die ganz ähnlich auch in Stringliteralen funktionieren):
Mit $Variable
kann der Wert einer zuvor definierten Variablen eingefügt werden.
Für komplexere Ausdrücke schreibe man \(Ausdruck)
.
Wenn mehrere Elemente eingefügt werden sollen, kann man \{Anweisungen}
schreiben. Die Anweisungen
sind dabei
Programmcode, der Werte berechnet und in den aktuellen Kontext einfügt, hier also in die Elementliste (Bzw. im Falle von Stringliteralen in den erzeugten String).
Die bevorzugte Methode, um Daten in den aktuellen Kontext einzufügen, stellt das Proxy-Objekt mit dem Namen context
dar, für das der
Operator <<
(in Anlehnung an C++) überladen wurde. Damit kann man zum Beispiel schreiben:
def domObject2 = <div>
\{
for i in 0..3:
context << '\n' <<
<p>
2*$i = \(2*i)
</p>
}
</div>
Das ergibt die Dom
-Datenstruktur
[:div, "\n ",
'\n', [:p, "\n 2*", 0, " = ", 0, "\n "]
'\n', [:p, "\n 2*", 1, " = ", 2, "\n "]
'\n', [:p, "\n 2*", 2, " = ", 4, "\n "]
"\n"
]
Wenn man diese Datenstruktur in einen String umwandeln und ausgeben lässt, bekommt man:
<div>
<p>
2*0 = 0
</p>
<p>
2*1 = 2
</p>
<p>
2*2 = 4
</p>
</div>
Die korrekte Einrückung der <p>
-Tags wird durch einen zusätzlichen Trick erreicht, den ich hier nicht weiter beschreibe.
Jede nichtleere Kombination einer gewissen Menge von Zeichen kann als Operator verwendet werden.
Außerdem sind
und in
Operatoren. Manche Operatoren, zum Beispiel ..
, in
oder =
, werden in manchen syntaktischen
Situationen auch als Schlüsselworte benutzt.
..
Binäre Infix-Operatoren werden durch Angabe des Operatornamens, der Präzedenz, der Assoziativität und einer Funktion definiert.
Bei der Assoziativität gibt es neben links-, rechts- und unassoziativen Operatoren auch noch Vergleichsoperatoren und Multioperatoren.
Anwendungen von Vergleichsoperatoren lassen sich zusammenfassen, sodass man beispielsweise den Ausdruck 1 < a + b <= c == 10
bilden kann, der bis auf die lediglich einmalige Auswertung von a + b
äquivalent zum Ausdruck
1 < a + b && a + b <=c && c == 10
ist.
Multioperatoren erlauben es, für mehrere ungeklammert nebeneinanderstehende Anwendungen desselben Operators einen einzigen Funktionsaufruf an eine variadische Funktion
zu tätigen. Zum Beispiel kann man das Supremum dreier Werte mit a | b | c
berechnen.
Partielle Anwendung von ungeklammert nebeneinanderstehenden Operatoren ist auch möglich (ähnlich wie übrigens auch partielle Funktionsanwendung).
Dabei werden ein oder mehr Operanden durch das Zeichen .
ersetzt.
Das Ergebnis eines solchen Ausdrucks ist eine Funktion mit so vielen Parametern, wie Operanden durch Punkte ersetzt wurden.
Also ist zum Beispiel .+1
bis auf die Parameternamen äquivalent zur Nachfolgerfunktion fn(x){return x+1}
und a*.+1/.
entspricht der Funktion
fn(x, y){return a*x+1/y}
.
Die meisten unären Operatoren lassen sich sowohl als Präfix- als auch als Postfix-Operatoren anwenden.
Die Postfix-Schreibweise verträgt sich besser mit Funktionsaufrufen und Attributzugriffen, während die Präfix-Schreibweise
insbesondere beim Minus, aber auch beim Prä-Inkrement-, Prä-Dekrement- und Adressoperator, Tradition hat.
In Postfix-Schreibweise steht allen Operatoren außer Post-Inkrement (++
) und Post-Dekrement (--
) ein Punkt voran, was die Ähnlichkeit
zu Methodenaufrufen noch erhöht. Außer <
gibt es jeden mit Punkt geschriebenen Postfix-Operator auch in Präfix-Form, dann ohne Punkt.
Präfix-<
gibt es nicht, weil das mit der Notation für XML-Ausdrücke in Konflikt stehen würde.
Außerdem: Wer braucht schon einen unären <
-Operator.
Wenn ein Programmiererdefinerbarer unärer Operator angewendet werden soll, wird zunächst geprüft, ob der Operand eine Methode dieses Namens hat, die dann gegebenenfalls aufgerufen wird. Andernfalls wird im aktuellen Scope nach einer entsprechend benannten Funktionsdefinition gesucht. Auf diese Weise kann man unäre Operatoren definieren, die mit allen Datentypen funktionieren, sich aber für einzelne Objekte mit spezialisierten Implementierungen überschreiben lassen.
Nachdem man eine lokale Variable einmal definiert hat, kann man ihren Wert aus Sicherheitsgründen nicht mehr verändern. Falls man das dennoch tun möchte, muss man die Variable als veränderbare Speicherzelle definieren:
def a = 0
def b = var 0
Der Name b
ist danach an eine veränderbare Speicherzelle gebunden, deren Wert zu 0
initialisiert wurde, aber später noch verändert
werden kann, während der Name a
direkt an den Wert 0
gebunden wird.
Wenn ein Lesezugriff auf b
irgendwo in einem Ausdruck auftritt, wird die mit b
verknüpfte Speicherzelle dereferenziert.
Bei einem Zugriff auf a
wird nicht dereferenziert, da es sich nicht um eine Speicherzelle handelt.
Man kann also def c = a + b
schreiben und muss sich keine Gedanken darüber machen, dass b
eigentlich keine Zahl, sondern nur ein Zeiger
auf eine Zahl ist.
Der Unterschied tritt dann zutage, wenn die Variablen auf der linken Seite einer Zuweisung verwendet werden:
b <- 3 + b
a <- 3 + a
Die erste Zeile funktioniert folgendermaßen: Der Wert des Namens b
wird nachgeschlagen, aber nicht dereferenziert, obwohl er eine Speicherzelle/Referenz ist.
Das Ergebnis der linken Seite ist also die Speicherzelle selbst. Dann wird die rechte Seite wie gewohnt ausgerechnet.
Anschließend wird die Methode set
der Speicherzelle aufgerufen, um den neuen Wert zu schreiben.
Die zweite Zeile produziert dagegen eine Fehlermeldung, weil a
keine set
-Methode hat.
Der nicht redefinierbare unäre Operator &
wertet seinen Operanden so aus, als würde er auf der linken Seite einer Zuweisung
stehen, unterdrückt also das automatische Dereferenzieren. Damit lassen sich Aliase definieren:
def d = &b
Die Variablen b
und d
sind dann an dieselbe Referenz gebunden.
Wenn ein Tupel-, Listen- oder Dom-Ausdruck auf der linken Seite einer Zuweisung steht, werden alle Elemente (und Attribute) ohne automatische Dereferenzierung
ausgewertet. Außerdem haben Tupel, Listen und Dom
-Objekte eine set
-Methode, die ihr Argument auseinandernimmt und
die Teile an die set
-Methoden der entsprechenden Elemente und Attribute weiterreicht.
Damit lässt sich beispielsweise eine Funktion zum Vertauschen der Werte zweier Referenzen folgendermaßen definieren und verwenden:
def swap(a, b){
(a, b) <- (b, a)
}
def x = var 1
def y = var 2
swap(&x, &y)
Die Attribute von Dom
-Objekten und die Elemente von Listen und Dom
-Objekten sind auch in veränderbare Speicherzellen verpackt.
Deswegen kann man einzelnen Elementen oder Attributen Alias-Namen geben, über die man sie auch verändern kann:
def liste=[0,1,2,3]
for n in varIterator(liste):
++n
Das erzeugt die Liste [1,2,3,4]
.
Die Standardbibliotheksfunktion varIterator
ist hier nötig, weil die normale Iteration über eine Liste die Variable n
nur an die in den
Speicherzellen der Liste enthaltenen Werte, nicht an die Speicherzellen selbst binden würde.
varIterator
ist definiert durch
def varIterator(l)[yield &l[i] : i in 0 .. l.size()]
und macht aus einer Liste ein iterierbares Objekt, das beim Iterieren mit konstantem Speicheroverhead die Referenzen auf die Listenelemente liefert.
def a = 3
ist a
ein Pattern, dass auf alles passt und das gematchte Objekt an den Namen a
bindet.
In der Anweisung
def (b, c) = f(a)
ist (b, c)
ein Pattern, das nur auf Paare von Werten passt und die Elemente des Paares an die Namen b
und c
bindet.
Wenn die Funktion f
hier etwas anderes als ein Paar zurückgibt, wird eine Exception PatternMatchFailure
ausgeworfen.
def f = fn(a@ !null, list@[head@in Int && positive(), .. rest@in Int]){
return (a.toString()*head, rest)
}
Diese Funktion erwartet als erstes Argument irgendein Objekt, Hauptsache es ist nicht null
. Das zweite Argument muss eine Liste mit mindestens einem
Element sein, wobei das erste Element head
genannt wird und eine positive ganze Zahl sein muss und die restlichen Elemente in einer Liste rest
zusammengefasst werden und ganze Zahlen sein müssen. Wenn ein Parameter-Pattern nicht auf das entsprechende Argument passt, wird eine Exception
ArgMismatchException
ausgeworfen.
case
-Ausdrücken im Zusammenhang mit der eingebauten Funktion match
: Wenn man ein Objekt analysieren und in Abhängigkeit von seiner
Struktur unterschiedliche Aktionen durchführen oder Ausdrücke auswerten will, kann man die match
-Funktion verwenden.
match(x,
case null:
context << "x ist null",
case in Int && modulo(y: 3):
context << "x und $y sind kongruent modulo 3",
case [y]:
context << "x ist eine Liste die nur ein $y enthält",
case _:
context << "x ist keine ganze Zahl oder einelementige Liste",
)
def collatz=fn(n@in Int&&positive()) match(n,
case even(): n//2,
case odd(): 3*n+1,
)
Wie man sehen kann, bestehen case
-Ausdrücke aus einem Pattern und einem Ausdruck. Die Funktion match
probiert die
Pattern der übergebenen case
-Objekte der Reihe nach durch und wertet den Ausdruck aus, der zum ersten passenden Pattern gehört.
Wenn kein Pattern passt, wird eine PatternMatchFailure
-Exception ausgeworfen.
Als Besonderheit von BHP sind case
-Ausdrücke nicht, wie bei anderen Sprachen, syntaktisch an die Verwendung im Rahmen eines match
-artigen
Konstruktes gebunden.
Man kann case
-Objekte zum Beispiel in dynamisch veränderbaren Listen speichern und dann diese Listen an match
übergeben.
Mit diesem Mechanismus ist in BHP beispielsweise die erweiterbare Operatorüberladung realisiert: Damit ein Operator neue Operandentypen verarbeiten kann,
wird der entsprechenden Liste ein case
-Objekt hinzugefügt.
for
-Schleifen: Hier werden, wie auch bei Variablen- und Parameterdefinitionen, lokale Variablennamen an Werte gebunden, nur dass die Werte aus einem
iterierbaren Objekt kommen.
for (index, element) in withIndex(list):
context << "list[$index] = $element\n"
Hier wird aus einer Liste mit Hilfe der Bibliotheksfunktion withIndex
ein iterierbares Objekt erzeugt, dass beim Iterieren
neben dem Listenelement auch noch den Index angibt.
Index und Element werden dann von dem Pattern (index, element)
in jedem Schleifendurchlauf an lokale Variablen gebunden,
die jeweils für die Dauer einer Iteration der Schleife gültig sind.
Falls ein vom Iterator geliefertes Objekt nicht auf das Pattern passen sollte, wird die Iteration für dieses Objekt übersprungen.
So kann man zum Beispiel über alle nicht-null
-Elemente einer Liste iterieren:
for element@ !null in list:
element.doSth()
for
-Schleife.
Die zweite Möglichkeit kann benutzt werden, um Zwischenergebnisse an Variablen zu binden und die erzeugte Liste patternbasiert zu filtern.
Folgende List Comprehension macht aus einer Liste list1
eine Liste der Stringrepräsentationen der Elemente von list1
,
wobei nur Elemente berücksichtigt werden, die nicht null
sind und deren Stringrepräsentation nicht leer ist.
def list2=[elemStr:
element@ !null in list1, #1. Möglichkeit
elemStr@ !"" = element.toString(), #2. Möglichkeit
]
let
-Ausdrücken. let
-Ausdrücke sind nur eine weitere Möglichkeit, lokale Variablen zu definieren.
catch
-Klauseln: Ob die zu behandelnde Exception auf das Pattern der catch
-Klausel passt, entscheidet darüber, ob die Klausel
zur Fehlerbehandlung herangezogen wird. Nebenbei können Bestandteile der Exception extrahiert und an lokale Variablen gebunden werden:
try:
readSettings()
catch IOException(msg):
printErr("Ein-/Ausgabe-Fehler: $msg")
modulo(y: 3)
, even()
und IOException(msg)
verwendet.
Bei diesen handelt es sich um Aufrufe von sogenannten Pattern-Abstraktionen.
Pattern-Abstraktionen dienen ähnlich wie Scalas Extraktor-Objekte dazu, selbstdefinierte Pattern-Bausteine verwenden zu können.
Zum Beispiel lässt sich die Pattern-Abstraktion twice
so definieren:
def case twice():(n//2) <- n@even()
Wenn die Abstraktion angewendet wird (zum Beispiel im Pattern twice(odd())
),
dann wird das Pattern rechts vom <-
mit dem gematchten Objekt verglichen.
Wenn es passt (was hier der Fall ist, wenn das Objekt eine gerade Zahl ist; als Nebeneffekt wird das Objekt n
genannt),
wird der Ausdruck n//2
ausgewertet (Integerdivision durch 2) und mit dem Pattern in der Argumentlisteder Abstraktionsanwendung verglichen, also im Beispiel hier mit
odd()
. Das Pattern twice(odd())
passt also auf alle Zahlen, die das Doppelte einer ungeraden Zahl sind.
Beim Pattern modulo(y: 3)
wird hinter dem Doppelpunkt noch eine gewöhnliche Argumentliste aus Ausdrücken übergeben.
Diese Argumente werden an die Parameter gebunden die vor dem Doppelpunkt der Pattern-Abstraktions-Definition stehen:
def case modulo(m):(o%m) <- o
def case even():() <- modulo(0:2)
def case odd():() <- modulo(1:2)
_ist, dann passiert nichts). Wenn die Variable schon gebunden wurde, gibt es je nach syntaktischem Kontext des Pattern entweder einen Syntaxfehler oder das gematchte Objekt wird mit dem gebundenen Wert verglichen; das Pattern passt dann nur, wenn die Werte gleich sind.
null
, true
, false
, eine Zahl- oder Stringkonstante.
Es passt nur auf ein Objekt, das den gleichen Wert wie die Konstante hat.
$
gefolgt von einem Variablennamen oder einem eingeklammerten Ausdruck.
Der Ausdruck bzw. Variablenname wird ausgewertet und wie beim ConstPattern mit dem gematchten Objekt verglichen.
&&
oder @
verknüpft sind.
Es passt nur, wenn alle Operanden passen. Alle von den Operanden vorgenommenen Variablenbindungen werden beibehalten.
||
verknüpft sind.
Es passt nur, wenn eines der Operandenpattern passt. Nur diejenigen vom ersten passenden Operanden vorgenommenen Variablenbindungen werden beibehalten,
die auch von allen Operanden vorgenommen worden wären.
!
-Zeichen gefolgt von einem Pattern. Es passt genau dann, wenn das andere Pattern nicht passt.
Es werden keine Variablen gebunden.
in
gefolgt von einem Ausdruck, der eingeklammert werden muss wenn er Infixoperatoren enthält.
Es passt, wenn das gematchte Objekt in dem Wert des Ausdrucks enthalten ist (nach Angabe von dessen contains
-Methode).
Damit lässt sich insbesondere die Zugehörigkeit zu einem bestimmten Typ prüfen.
var
gefolgt von einem Pattern. Es passt auf veränderbare Speicherzellen, deren aktueller Inhalt auf
das andere Pattern passt.
if
gefolgt von einem Ausdruck.
Es passt, wenn der Wert des Ausdrucks, in einen Wahrheitswert konvertiert, true
ergibt.
let
gefolgt von einem Pattern, einem =
-Zeichen und einem Ausdruck.
Es passt, wenn der Wert des Ausdrucks auf das Pattern passt, und behält die daraus resultierenden Variablenbindungen bei.
Im folgenden Beispiel wird es verwendet, um eine Pattern-Abstraktion zu definieren, die auf Strings passt, welche ganze Zahlen darstellen, und
die es erlaubt, die geparste Zahl gegen ein weiteres Pattern zu matchen:
def case intString():(value) <- o @ let (value @ !null) = tryParseInt(o)
def tryParseInt(n){
try:
return parseInt(n)
catch _:
return null
}
context << match(str,
case intString(n@even()): "Stringdarstellung der geraden Zahl $n",
case intString(n@odd()): "Stringdarstellung der ungeraden Zahl $n",
case _: "Keine Stringdarstellung einer ganzen Zahl",
)
Argumentliste, wie oben beschrieben, oder es ist die Anwendung eines selbstdefinierten Pattern-Operators auf ein oder zwei Pattern. Statt Namen von Pattern-Abstraktionen können auch Namen von Extraktor-Objekten verwendet werden, die ich hier aber nicht beschreibe.
%Pattern
enthalten.
Das extrahiert die Länge des Tupels und matcht sie gegen das angegebene Pattern.
Als letztes Element kann ein TuplePattern das Schlüsselwort ..
optional gefolgt von einem Pattern enthalten.
Dies bedeutet, dass noch beliebig viele Elemente kommen können. Wenn ein Pattern hinter dem ..
angegeben wurde, müssen die Restelemente
alle einzeln auf dieses Restpattern passen.
Die dabei jeweils gebundenen Variablen werden zu Listen zusammengefasst und die entsprechenden Namen werden vom TuplePattern an diese Listen gebunden.
Beispiel: Das Pattern
((fst0, snd0), %odd(), .. (fsts, snds))
passt auf alle Tupel, die aus Paaren bestehen und ungerade Länge haben. Die Elemente des ersten Paares werden an fst0
und snd0
gebunden.
Die ersten Elemente der restlichen Paare werden zu einer Liste namens fsts
zusammengefasst und die zweiten Elemente zu einer Liste snds
.
*-Zeichen vorangestellt werden, um es gegen die dem Listenelement zugrundeliegende veränderbare Speicherzelle zu matchen und nicht gegen deren Wert.
Dom
-Objekte.
Eine solche Angabe kann sein:
Dom
-Objekts mit einem von ihnen
übereinstimmen.
=
-Zeichen und einem Pattern. Das Tag des Dom
-Objekts muss auf das Pattern passen.
=
oder ->
und einem Pattern. Dies matcht das genannte Attribut des Dom
-Objekts
gegen das Pattern. Vor dem Pattern kann wieder ein *stehen, um zu bewirken, dass gegen die zum Attribut gehörende veränderbare Speicherzelle gematcht wird.
. * =
oder . * ->
gefolgt von einem Pattern. Dies extrahiert die Liste der Elemente und matcht sie gegen das angegebene Pattern.
fn
gefolgt von einer eingeklammerten Parameterliste.
Es sieht also genauso aus wie ein Lambda-Funktionskopf.
Es passt auf ein Dom
-Objekt, das, wenn es wie eine Argumentliste behandelt wird, an die Parameter in der Parameterliste
gebunden werden kann, ohne dass dabei eine ArgMismatchException entsteht.
Die Kindelemente des Dom
-Objekts geben die positionalen Argumente an und die Attribute die benannten Argumente.
ParamPattern dienen hauptsächlich dazu, gegen Argumentlisten zu matchen, die in variadischen Funktionsköpfen entstehen, um polymorphe Funktionen zu realisieren:
def f(args*.){
# Positionale und benannte Argumente werden
# im Dom-Objekt 'args' zusammengefasst
match(args,
case fn(c, d)&&let []=d: printOut("c=$c, d=$d (Spezialfall)"),
case fn(h, i): printOut("h=$h, i=$i"),
case fn(c, d): printOut("c=$c, d=$d"),
case fn(a =100, b=200): printOut("a=$a, b=$b"),
case fn(e, f, g): printOut("e=$e, f=$f, g=$g"),
case _: printOut("Nichts passt auf $args")
)
}
f() # Schreibt "a=100, b=200"
f(1, 2) # Schreibt "h=1, i=2"
f(1, []) # Schreibt "c=1, d=[] (Spezialfall)"
f(1, .d=2) # Schreibt "c=1, d=2"
f(1,2,3) # Schreibt "e=1, f=2, g=3"
f(1,2,3,.x=4) # Schreibt "Nichts passt auf [:(null), .x = 4, 1, 2, 3]"
In seiner Grundform besteht ein QuantifiedPattern aus einem Pattern gefolgt von einem Quantor, das ist eine Angabe in geschweiften Klammern.
Das gematchte Objekt muss eine endliche Sequenz repräsentieren und zu diesem Zweck die Methoden size
, prefix
und
suffix
bereitstellen, mit denen es sich zerlegen lässt. Es kann also zum Beispiel ein String, eine Liste oder ein Tupel sein.
Das QuantifiedPattern passt, wenn das gematchte Objekt die Konkatenation von mehreren Teilsequenzen ist, die einzeln auf das Pattern vor dem Quantor passen.
Die dabei gebundenen Variablen werden zu Listen zusammengefasst. Der Quantor gibt an, wie viele Teilsequenzen es sein dürfen, und kann verschiedene Formen annehmen:
{Ausdruck}
: Der Ausdruck muss eine nichtnegative Ganzzahl ergeben. Es muss genau so viele Teilsequenzen geben,
wie diese Zahl ist, sonst passt das QuantifiedPattern nicht.{minAusdruck,}
: Der Ausdruck muss eine nichtnegative Ganzzahl ergeben. Es muss mindestens so viele Teilsequenzen geben,
wie diese Zahl ist, sonst passt das QuantifiedPattern nicht.{minAusdruck,maxAusdruck}
: Die Ausdrücke müssen nichtnegative Ganzzahlen ergeben (Wenn maxAusdruck
null
ergibt,
ist das so, als würde gar nichts hinter dem Komma stehen). Es muss mindestens so viele Teilsequenzen geben,
wie minAusdruck
angibt, und höchstens so viele, wie maxAusdruck
angibt, sonst passt das QuantifiedPattern nicht.case
gefolgt von einem Pattern stehen, das gegen die Anzahl der Teilsequenzen
gematcht wird, um zu entschieden, ob das QuantifiedPattern passt. Variablenbindungen aus diesem Matching werden nicht beibehalten
Ein QuantifiedPattern mit dem Quantor {0,}
, {1,}
bzw. {0,1}
lässt sich auch mit den unären Pattern-Operatoren
*
, +
bzw. ?
schreiben.
Der Matching-Algorithmus für QuantifiedPattern funktioniert so, dass von der Sequenz der Reihe nach Präfixe unterschiedlicher Größe abgespalten werden.
Wenn eines davon auf das Teilsequenzpattern passt, wird rekursiv auf dem dazugehörigen Suffix weitergematcht. Der Algorithmus terminiert erfolgreich,
wenn das Suffix leer ist und die gefundene Anzahl von Teilsequenzen zum Quantor passt.
Damit der Algorithmus durch das Abspalten von leeren Präfixen nicht in eine Endlosschleife geraten kann, werden unter entsprechenden
Umständen nie zwei leere Präfixe direkt hintereinander abgespalten. Bis auf diese Einschränkung probiert der Algorithmus aber alle Zerlegungen der Sequenz aus,
bis eine passende gefunden wird. Zusammen mit dem OrPattern und den Pattern-Operatoren +
und <+
für das Aufspalten von Sequenzen (aus der Standardbibliothek)
erreichen BHP-Pattern die Ausdrucksfähigkeit von regulären Ausdrücken. Diese Implementierung ist allerdings nicht besonders effizient.
Nimmt man noch AbstractionApplicationPattern hinzu, so sollte man auch kontextfreie Sprachen erkennen können. Aber ohne Caching von Matching-Ergebnissen
(das ich nicht implementiert habe) wird das richtig ineffizient, und für ernsthafte Parser taugt es auch deshalb nichts,
weil bei Syntaxfehlern in der Eingabe das Pattern einfach nicht passt anstatt eine informative Fehlermeldung zu erzeugen.
Hier ist ein Beispiel für die Verwendung von QuantifiedPattern:
match("----|Hallo|Welt|",
case "-"{case odd(),4} <+ (b + "|").* :
printOut("b=$b"), # Schreibt "b=[-, Hallo, Welt]"
)
Der Pattern-Operator <+
passt auf eine Sequenz, wenn seine Operanden nacheinander auf die Sequenz passen, wobei versucht wird,
den ersten Operanden auf einen möglichst großen Teil der Sequenz zu matchen.
Hier ist ein umfangreicheres Beispiel aus dem Konstruktor meiner TreeMap-Implementierung:
def dispatch = fn(args*)match(args,
# Mache leere TreeMap
case []: ctor(var null),
# Mache leere TreeMap mit Nichtstandard-Comparator
case [comp@in Comparator]: ctor(var null, comp),
# Kopiere eine TreeMap
case [map@in TreeMap]: ctor(var map.getTree(), map.comparator()),
# Initialisiere TreeMap mit Schlüssel-Wert-Paaren aus einem Iterable
case [it@iterable()]: dispatch(it, stdComparator),
# Mache TreeMap mit Nichtstandard-Comparator, initialisiert aus Iterable
case [it@iterable(), comp@in Comparator]: {
def r = ctor(var null, comp);
r.putAll(it);
r;
},
case [comp@in Comparator, it@iterable()]: dispatch(it, comp),
# Mache TreeMap mit Nichtstandard-Comparator, initialisiert mit
# Schlüssel-Wert-Paaren aus den restlichen Argumenten
case [comp@in Comparator, .. kv@(_,_)]: dispatch(kv, comp),
# Initialisiere TreeMap mit Schlüssel-Wert-Paaren aus der Argumentliste
case [.. kv@(_,_)]: dispatch(kv, stdComparator),
# Mache TreeMap mit Nichtstandard-Comparator, initialisiert mit
# Schlüsseln und Werten aus den restlichen Argumenten,
# die dort nicht zu Paaren zusammengefasst sind.
case [comp@in Comparator,%odd() , .. kv]:
dispatch([yield (key, value): key, value in kv], comp),
# Initialisiere TreeMap mit
# Schlüsseln und Werten aus den Argumenten,
# die dort nicht zu Paaren zusammengefasst sind.
case [%even(), .. kv]:
dispatch([yield (key, value): key, value in kv], stdComparator),
# Ungültige Argumentliste
case _: error(
"TreeMap() does not understand these argument types",
ArgMismatchException()
)
)
Die variadische Funktion dispatch
versteht verschiedene Argumentlistenformate und wird von dem nach außen sichtbaren Konstruktor aufgerufen.
Ich habe das hier geschrieben, bevor es ParamPattern gab, daher benutzt es nur positionale Argumente.
Der Funktionskopf fn(args*)
bindet die gesamte Argumentliste an den Namen args
.
Anschließend wird die Argumentliste mit verschiedenen ListPattern verglichen. Je nachdem, welches Pattern passt, werden die Argumente
transformiert und mittels der eigentlichen (nicht-variadischen) Konstruktorfunktion ctor
und der Methode putAll
zum Aufbau einer TreeMap verwendet.
Der vorletzte case
-Ausdruck erlaubt es zum Beispiel zu schreiben:
TreeMap(schlüssel1, wert1, schlüssel2, wert2, schlüssel3, wert3)
Damit zu jedem Schlüssel ein Wert gehört, muss die Argumentanzahl gerade sein.
Das Pattern des vorletzten case
-Ausdrucks passt wegen %even()
nur dann, wenn tatsächlich eine gerade Anzahl von Argumenten übergeben wurde.
Diese werden dann mit der Iterable Comprehension [yield (key, value): key, value in kv]
zu Paaren zusammengefasst.