Inhalt

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.

XML-Ausdrücke

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 class mit dem Wert "someClass" und attr mit dem Wert 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.

Operatoren

Jede nichtleere Kombination einer gewissen Menge von Zeichen kann als Operator verwendet werden. Außerdem sind in und .. 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.

Referenzen

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.

Pattern Matching

Mit Pattern Matching lässt sich prüfen, ob ein Datenobjekt einer vorgegebenen Struktur entspricht, und als Nebeneffekt werden die Bestandteile des Objekts an Namen gebunden.

Verwendung

BHP verwendet Pattern Matching in folgenden Situationen:

Arten von Pattern

Pattern können mit verschiedenen (auch selbstdefinierten) Operatoren aus elementaren Pattern zusammengesetzt werden. Die Operatoren und Elementarpattern im Einzelnen sind:
BindPattern
Ein BindPattern ist einfach ein Variablenname. Wenn die Variable im aktuellen Scope noch nicht definiert wurde, passt das Pattern auf alles und bindet das gematchte Objekt an die Variable (Außer wenn der Variablenname _ 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.
ConstPattern
Ein ConstPattern ist eine Konstante wie null, true, false, eine Zahl- oder Stringkonstante. Es passt nur auf ein Objekt, das den gleichen Wert wie die Konstante hat.
EqualsPattern
Ein EqualsPattern besteht aus einem $ gefolgt von einem Variablennamen oder einem eingeklammerten Ausdruck. Der Ausdruck bzw. Variablenname wird ausgewertet und wie beim ConstPattern mit dem gematchten Objekt verglichen.
AndPattern
Ein AndPattern besteht mehreren Pattern, die mit den Operatoren && oder @ verknüpft sind. Es passt nur, wenn alle Operanden passen. Alle von den Operanden vorgenommenen Variablenbindungen werden beibehalten.
OrPattern
Ein OrPattern besteht mehreren Pattern, die mit dem Operator || 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.
NotPattern
Ein NotPattern besteht aus einem !-Zeichen gefolgt von einem Pattern. Es passt genau dann, wenn das andere Pattern nicht passt. Es werden keine Variablen gebunden.
InPattern
Ein InPattern besteht aus dem Schlüsselwort 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.
VarPattern
Ein VarPattern besteht aus dem Schlüsselwort var gefolgt von einem Pattern. Es passt auf veränderbare Speicherzellen, deren aktueller Inhalt auf das andere Pattern passt.
ConditionalPattern
Ein ConditionalPattern besteht aus dem Schlüsselwort if gefolgt von einem Ausdruck. Es passt, wenn der Wert des Ausdrucks, in einen Wahrheitswert konvertiert, true ergibt.
LetPattern
Ein LetPattern besteht dem Schlüsselwort 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",
)
AbstractionApplicationPattern
Ein AbstractionApplicationPattern ist entweder der Name einer Pattern-Abstraktion gefolgt von einer 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.
TuplePattern
Ein TuplePattern besteht im Wesentlichen aus 0 oder mehr kommaseparierten Pattern innerhalb von runden Klammern. Wenn es lediglich ein einziges Pattern in Klammern ist, muss ein Komma dahinterstehen, damit es als 1-Tupel und nicht als simples eingeklammertes Pattern geparst wird. Optional kann ein TuplePattern auch noch eine Angabe der Form %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.
ListPattern
Ein ListPattern funktioniert so ähnlich wie ein TuplePattern mit eckigen statt runden Klammern. Es kann einem Element-Pattern aber noch ein *-Zeichen vorangestellt werden, um es gegen die dem Listenelement zugrundeliegende veränderbare Speicherzelle zu matchen und nicht gegen deren Wert.
DomPattern
Ein DomPattern besteht aus mehreren kommaseparierten Angaben in eckigen Klammern und passt nur auf Dom-Objekte. Eine solche Angabe kann sein:
  • Ein Doppelpunkt gefolgt von einem Tag-Namen. Wenn ein oder mehrere dieser Angaben präsent sind, muss das Tag des Dom-Objekts mit einem von ihnen übereinstimmen.
  • Ein Doppelpunkt gefolgt von einem =-Zeichen und einem Pattern. Das Tag des Dom-Objekts muss auf das Pattern passen.
  • Ein Punkt, gefolgt von einem Attributnamen, einem = 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.
  • Die Sequenz . * = oder . * -> gefolgt von einem Pattern. Dies extrahiert die Liste der Elemente und matcht sie gegen das angegebene Pattern.
ParamPattern
Ein ParamPattern besteht aus dem Schlüsselwort 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]"
QuantifiedPattern

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.
  • Anstelle des ersten Ausdrucks kann auch das Schlüsselwort 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
  • Wenn nach der öffnenden Klammer ein Doppelpunkt steht, versucht der Matching-Algorithmus zuerst möglichst große Präfixe von der Sequenz abzuspalten anstatt wie normal möglichst kleine.

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.

Anwendungsbeispiel

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.

Teilen:  E-Mail Twitter Facebook LinkedIn Reddit
Impressum und Kontakt | © 2016-08-07 siquod.org

Diskussion

Bisher keine Beiträge

Kommentar abgeben


:
-- Gib den voranstehenden Text ein: