Dieser Artikel wurde maschinell übersetzt.

Die Working Programmer

Parser Combinators

Ted Neward

Ted NewardMit dem Abschluss der multiparadigmatic Serie schien es Zeit, sich in neue Wege zu riskieren. Wie das Schicksal es würde, jedoch einiges Client vor kurzem ließ mich mit interessanten Material, das trägt Diskussion, bezieht sich auf das Design der Software, fungiert als ein weiteres Beispiel für die Kommunalität/Variabilität Analyse das Herzstück der multiparadigmatic-Serie, und... nun, am Ende, es ist nur wirklich cool.

Das Problem

Ich habe Client ist Hals tief in die Welt der Neuro-optische Wissenschaft und fragte mich, mit ihm arbeiten an einem neuen Projekt zur Durchführung Experimente auf optische Gewebe zu erleichtern. Speziell, arbeite ich auf einem Softwaresystem zu eine Mikroskop-Rig zu steuern, die verschiedenen Impulse (LEDs, Lampen und So weiter), die Antworten aus dem optischen Gewebe auslösen treiben wird, und erfassen dann die Ergebnisse gemessen durch Hardware gerade das optische Gewebe.

Wenn es vage Matrix-y für Sie klingt, sind Sie nicht ganz allein. Als ich zuerst hörte über dieses Projekt, meine Reaktion war gleichzeitig, "Oh, Wow, das ist cool!" und "Oh, warte, ich nur warf in den Mund ein wenig."

Auf jeden Fall ist eines der wichtigsten Dinge über die Anlage, die wird es eine ziemlich komplexe Konfiguration für jeden Versuch ausführen zugeordnet, und das führte uns so an, dass die Konfiguration zu betrachten. Auf der einen Seite, schien es ein offensichtliches Problem für eine XML-Datei. Jedoch sind nicht die Menschen laufen die Anlage geht auf Computer-Programmierer, sondern Wissenschaftler und Lab Assistenten, so es ein wenig ungeschickt schien zu erwarten, dass sie schreiben wohlgeformte XML-Dateien (und bekommen es richtig an jeder Ecke). Der Gedanke an eine Art von GUI-basierte Konfigurationssystem produziert schlug uns als sehr over-engineered, zumal es schnell in Diskussionen um offene Arten von Daten zu erfassen, wie wir verwandeln würde.

Am Ende schien es angebracht zu verleihen eine benutzerdefinierte Konfiguration-Format, die Tonnen von Analysieren von Text auf meiner Seite. (Einige, würde dies bedeuten, dass ich eine DSL baue; Dies ist eine Diskussion am besten Links, Philosophen und anderen Beteiligten in der schweren Aufgabe des Alkoholkonsums.) Glücklicherweise gibt es Lösungen in diesem Bereich.

Gedanken

Ein Parser dienen zwei interessante und nützliche: Konvertieren von Text in eine andere, aussagekräftigere Form und überprüfen/Überprüfen des Textes folgt eine Struktur (Dies ist normalerweise Teil dazu beizutragen, konvertiert es in eine aussagekräftigere Form). Also, hat z. B. eine Telefonnummer, die in seinem Herzen nur eine Folge von Zahlen ist, noch eine Struktur darauf, die Überprüfung erfordert. Das Format hängt von Kontinent zu Kontinent, aber die Zahlen sind immer noch zahlen. In der Tat eine Telefonnummer ist ein großartiges Beispiel für einen Fall, wo nicht die "aussagekräftigere Form" ein ganzzahliger Wert ist — die Ziffern sind kein Integer-Wert, sie sind eine symbolische Repräsentation, die in der Regel besser als ein Domain-Typ dargestellt. (Behandlung von ihnen als "nur" eine Reihe macht es schwierig, extrahieren Sie die Länder- oder Ortsvorwahl, zum Beispiel.)

Wenn eine Telefonnummer aus besteht Ziffern, und so sind numbers (Gehälter, Mitarbeiter-IDs usw.), dann wird es einige Duplizität im Code, wo wir analysieren und stellen Sie sicher stellen, es sei denn wir irgendwie einen Parser erweitern. Das bedeutet dann, dass wir unabhängig Parser möchten wir bauen zu offenen, so dass jemand mit der Parser/Bibliothek erweitern es auf verschiedene Arten (kanadischer Postleitzahlen, zum Beispiel) ohne die Quelle selbst ändern zu müssen. Dies ist bekannt als die "offen-geschlossen-Prinzip": Softwareentitäten sollten offen für Erweiterungen, aber geschlossen für Änderungen.

Lösung: Generative Metaprogrammierung

Eine Lösung ist die traditionelle "Lex/Yacc" Ansatz, bekannt mehr formal als "Parsergenerator." Dies beinhaltet die Syntax für die Konfigurationsdatei in einem abstrakten Format angeben — in der Regel Abwechslung auf der Backus-Naur form (BNF) Syntax/Grammatik verwendet, um formale Grammatik, z. B. was die Programmierung Sprachen nutzt beschreiben — dann läuft ein Werkzeug, um Code zum Generieren der Zeichenfolgeneingabe auseinander nehmen und eine Art von Struktur Strukturen oder Objekten als Folge ergeben. In der Regel Beteiligten dabei zwei Schritte, "lexikalische" und "Analyse" gliedert sich in in dem die Lexer zunächst die Zeichenfolge eingegeben in Token wandelt, überprüfen, dass die Zeichen in der Tat berechtigte Token auf dem Weg bilden. Dann der Parser nimmt die Token und überprüft, ob die Token erscheinen in der richtigen Reihenfolge und die entsprechenden Werte enthalten, und usw., Umwandlung von in der Regel die Token in einige Art abstract baumförmige Struktur für die weitere Analyse.

Die Probleme mit Compilerbau sind die gleichen für alle generative Metaprogrammierung Ansatz: der generierten Code müssen bei neu generiert werden, dass die Syntax ändert. Aber noch wichtiger ist für diese Art von Szenario, der generierten Code werden Computer-generierte, mit all der wunderbaren Variable benennen, die mit Computer-generierten Code (jemand bereit zu Fastfood-für Variablen wie z. B. "integer431" und "string$ $x$ y$ Z"?), kommt so schwierig zu debuggen.

Lösung: funktionale

In einer bestimmten Art von Licht, Analyse funktioniert grundsätzlich: Es führt eine Art von Vorgang nimmt die Eingabe und erzeugt eine Ausgabe als Folge. Die kritische Einsicht, es stellt sich heraus, dass ein Parser von vielen kleinen Parser erstellt werden kann, und von denen jeder analysiert ein klein wenig von der Zeichenfolgeneingabe gibt dann ein Token und eine andere Funktion das nächste bisschen Zeichenfolgeneingabe auszuwerten. Diese Techniken, die meines in Haskell eingeführt wurden Erachtens, sind formell als Parser Kombinatoren bekannt, und sie erweisen sich eine elegante Lösung für "mittlere" Analysieren von Problemen-Parser, die nicht unbedingt so komplex wie eine Programmiersprache erforderlich wäre, sondern etwas über hinaus was String.Split (oder ein gehackt-Up-Serie von Regex-Scans) tun kann.

Der Parser Kombinatoren wird die Anforderung öffnen für Erweiterung durch kleine Funktionen erstellen, dann verwenden funktionale Techniken, um "kombinieren" in größere Funktionen (das ist, wo wir die Namen "Kombinatoren") erreicht. Größere Parser können von jedem Benutzer mit ausreichenden Fähigkeit zu verstehen, Funktion Komposition bestehen. Diese Technik ist eine allgemeine, die Exploration trägt, aber ich werde zu speichern, die für einen zukünftigen Artikel.

Es stellt sich heraus, gibt es mehrere Parser Combinator Bibliotheken für die Microsoft.NET Framework, viele von ihnen auf der Grundlage von Moduls Parsec in Haskell, die Art von den Standard für Parser kombinatorische Bibliotheken setzen geschrieben. Zwei solche Bibliotheken sind FParsec, für f# und Sprache, geschrieben in c# geschrieben. Jeder ist open Source und relativ gut dokumentiert, so dass sie den Zweck des Seins beide nützlich out of the Box dienen und als ein Modell aus, um Design-Ideen zu untersuchen. Ich lasse auch FParsec für einen zukünftigen Artikel.

"Sprache Sie analysieren?"

Sprache, abrufbar unter code.google.com/p/sprache, beschreibt sich selbst als eine "einfache, kleine Bibliothek für den Bau von Parser direkt im C#-Code", die "'industrial Strength' Sprache Werkbänke Konkurrenz nicht. Es ist irgendwo zwischen regulären Ausdrücken und ein voll ausgestatteter Toolset z. B. ANTLR." (ANTLR ist ein Parser-Generator in der generativen Metaprogrammierung Kategorie wie Lex/Yacc.)

Erste Schritte mit Sprache ist einfach: Laden Sie den Code, erstellen Sie das Projekt, dann kopieren Sie die Sprache.dll-Assembly in Ihrem Projektverzeichnis Abhängigkeit und fügen Sie den Verweis auf das Projekt. Von hier, alle Parser Definition Arbeiten erfolgt durch Deklarieren von Sprache.Parser Instanzen und kombiniert sie auf besondere Weise zum Erstellen von Sprache.Parser Instanzen, die wiederum kann, wenn gewünscht (und es ist in der Regel), Domänenobjekte, die einige oder alle der analysierten Werte zurückgeben.

Einfache Sprache

Um zu beginnen, beginnen wir mit einem Parser, der weiß, wie Benutzer eingegebenen Telefonnummern in ein Domain-Typ PhoneNumber analysiert. Der Einfachheit halber, ich bleibe mit der U.S.-Stil format—(nnn) Nnn-Nnnn — aber wollen wir speziell die Aufschlüsselung in Vorwahlen, Präfix und Linie zu erkennen, und erlaubt für Briefe an die Stelle der Ziffern (so dass jemand ihre Telefonnummer als "(800) EAT-NUTS" eingeben Wenn sie wünschen). Im Idealfall wird der Domain-Typ PhoneNumber Alpha Formulare rein numerische on-Demand, konvertieren und aber die Funktionalität bleibt als eine Übung für dem Leser (d. h., im Wesentlichen, die ich nicht mit ihm die Mühe möchten).

(Die Pedant in mir darauf hinweisen, dass einfach konvertieren den Alphas zu zahlen eine vollständig kompatible Lösung, durch die Art und Weise ist nicht verlangt. In der Schule, es war üblich in meinem Freundeskreis zu versuchen, kommen mit Adressen, die "cool" Dinge geschrieben — ein Ex-Mitbewohner ist still wartend für 1-800-CTHULHU frei, wird in der Tat, so dass er für alle Ewigkeit das Spiel gewinnen kann.)

Die einfachste unterbringen zu Vorsprung ist mit der PhoneNumber Domain-Typ:

class PhoneNumber
{
  public string AreaCode { get; set; }
  public string Prefix { get; set; }
  public string Line { get; set; }
}

Waren dies eine "echte" Domain-Typ, Ortsvorwahl, Prefix und Linie hätte Bestätigungs-Code in ihre Eigenschaftensatz Methoden, aber dies würde dazu führen, eine Wiederholung der Code zwischen der Parser und der Domain-Klasse (die übrigens, wir beheben werde, bevor dies alles getan ist).

Als nächstes müssen wir wissen, wie man einen einfachen Parser erstellen, der weiß, wie man n die Anzahl der Ziffern zu analysieren:

public static Parser<string> numberParser =
  Parse.Digit.AtLeastOnce().Text();

Definieren der NumberParser ist einfach. Beginnen Sie mit dem primitiven Parser Digit (eine Instanz eines <T>-Parsers in der Sprache.Parse-Klasse definiert), und beschreiben Sie, dass wir wollen, dass mindestens eine Ziffer im Eingabestream, implizit verbraucht alle Ziffern bis die Eingabe-Stream entweder trocken läuft oder der Parser einem nicht numerischen Zeichen begegnet. Die Text-Methode konvertiert den Datenstrom der analysierten Ergebnisse in einer einzigen Zeichenfolge für unseren Verbrauch.

Dies ist recht einfach — feed es eine Zeichenfolge und lassen Sie 'er Rippen:

[TestMethod]
public void ParseANumber()
{
  string result = numberParser.Parse("101");
  Assert.AreEqual("101", result);
}
[TestMethod]
public void FailToParseANumberBecauseItHasTextInIt()
{
  string result = numberParser.TryParse("abc").ToString();
  Assert.IsTrue(result.StartsWith("Parsing failure"));
}

Wenn ausführen, speichert das Ergebnis "101". Wenn die Parse-Methode eine Eingabezeichenfolge von "Abc" gespeist wird, ergibt es eine Ausnahme. (Wenn die nonthrowing Verhalten bevorzugt wird, hat Sprache auch eine TryParse-Methode, die ein Ergebnisobjekt zurückgibt, die über Erfolg oder Misserfolg abgefragt werden können.)

Die Telefonnummer Analyse Situation ist ein wenig komplizierter, aber; Es muss nur drei oder vier Ziffern zu analysieren – nicht mehr und nicht weniger. Definieren einer solchen Parser (dreistellige Parser) ist ein bisschen schwieriger, aber immer noch machbar:

public static Parser<string> threeNumberParser =
  Parse.Numeric.Then(first =>
    Parse.Numeric.Then(second =>
      Parse.Numeric.Then(third =>
        Parse.Return(first.ToString() +
          second.ToString() + third.ToString()))));

Der numerische Parser wird ein Zeichen und wenn es eine Ziffer ist setzt auf das nächste Zeichen. Die dann Methode eine Funktion (in Form eines Lambda-Ausdrucks) annimmt, auszuführen. Die Return-Methode erfasst jede dieser zu einer einzigen Zeichenfolge und wie der Name schon sagt, verwendet, die als Rückgabewert (finden Sie unter Abbildung 1).

Abbildung 1 Analyse eine Telefonnummer

[TestMethod]
public void ParseJustThreeNumbers()
{
  string result = threeNumberParser.Parse("123");
  Assert.AreEqual("123", result);
}
[TestMethod]
public void ParseJustThreeNumbersOutOfMore()
{
  string result = threeNumberParser.Parse("12345678");
  Assert.AreEqual("123", result);
}
[TestMethod]
public void FailToParseAThreeDigitNumberBecauseItIsTooShort()
{
  var result = threeNumberParser.TryParse("10");
  Assert.IsTrue(result.ToString().StartsWith("Parsing failure"));
}

Erfolgreich So weit. (Ja, ist die Definition der ThreeNumberParser umständlich — sicher muss es einen besseren Weg, dies zu definieren! Keine Angst: es, aber um zu verstehen, wie den Parser erweitert, müssen wir tauchen Sie tiefer ein in wie Sprache aufgebaut ist, und das ist das Thema für den nächsten Teil dieser Serie.)

Jetzt müssen wir allerdings behandeln die Links-Pars, rechts -­Pars und den Bindestrich, und alles in ein PhoneNumber-Objekt zu konvertieren. Es mag ein wenig umständlich mit was wir sehen so weit, aber beobachten was passiert als nächstes, wie in gezeigt Abbildung 2.

Abbildung 2 Eingabe in ein PhoneNumber-Objekt konvertieren

public static Parser<string> fourNumberParser =
  Parse.Numeric.Then(first =>
    Parse.Numeric.Then(second =>
      Parse.Numeric.Then(third =>
        Parse.Numeric.Then(fourth =>
          Parse.Return("" + first.ToString() +
            second.ToString() + third.ToString() +
              fourth.ToString())))));
public static Parser<string> areaCodeParser =
  (from number in threeNumberParser
  select number).
XOr(
  from lparens in Parse.Char('(')
  from number in threeNumberParser
  from rparens in Parse.Char(')')
  select number);
public static Parser<PhoneNumber> phoneParser =
  (from areaCode in areaCodeParser
  from _1 in Parse.WhiteSpace.Many().Text()
  from prefix in threeNumberParser
  from _2 in (Parse.WhiteSpace.Many().Text()).
Or(Parse.Char('-').Many())
  from line in fourNumberParser
  select new PhoneNumber() { AreaCode=areaCode, Prefix=prefix, Line=line});
Using the parser becomes pretty straightforward at this point:
[TestMethod]
public void ParseAFullPhoneNumberWithSomeWhitespace()
{
  var result = phoneParser.Parse("(425) 647-4526");
  Assert.AreEqual("425", result.AreaCode);
  Assert.AreEqual("647", result.Prefix);
  Assert.AreEqual("4526", result.Line);
}

Bestes von allen, ist der Parser vollständig erweiterbar, da es, auch in einen größeren Parser geschrieben werden kann, die Texteingabe in eines Address-Objekts oder Kontaktdaten Objekt oder etwas anderes denkbar umwandelt.

Das Konzept der Kombinatorik

In der Vergangenheit wurde Text Parsen der Provinz "Sprache Forscher" und Wissenschaft, vor allem aufgrund der komplizierten und schwierigen bearbeiten-erzeugen-kompilieren-Test-Debug Zyklus mit generativen Metaprogrammierung Lösungen inhärent. Versuchen, zu Fuß durch Computer -­generierten Code – insbesondere die endliche-Zustand-Maschine-basierte Versionen, die viele Compilerbau Codeänderungen aus — in einem Debugger ist eine Herausforderung, auch die schweren Entwickler. Aus diesem Grund glaube nicht die meisten Entwickler über Lösungen entlang Linien als mit einem Text-basierten Problem analysieren. Und in der Wahrheit, die meiste Zeit, eine Parser-Generator-basierte Lösung wäre drastisch übertrieben.

Parser Kombinatoren dienen als eine schöne dazwischen Lösung: flexibel und leistungsfähig genug, um behandeln einige nicht-triviale analysieren, ohne dass ein Ph.d. in der Informatik zu verstehen, wie man sie benutzt. Noch interessanter, das Konzept der Kombinatorik ist faszinierend, und führt zu einige andere interessanten Ideen, die wir später untersuchen werde.

In der Geist, in dem diese Spalte geboren wurde, vergewissern Sie sich, dass "Auge" für meine nächste Spalte aus (sorry, konnte nicht widerstehen), in denen werde ich Sprache nur einen Hauch, Verringerung die Hässlichkeit der hier definierten drei und vier Ziffern Parser erweitern.

Glücklich Kodierung!

Ted Neward ist ein architektonischer Berater bei Neudesic LLC. Er hat mehr als 100 Artikel geschrieben und Autor oder Mitautor von einem Dutzend Bücher, einschließlich "Professionelle F#-2.0" (Wrox, 2010). Er ist C#-MVP und Vorträge auf Konferenzen auf der ganzen Welt. Er berät und Mentoren regelmäßig — erreichen ihn an ted@tedneward.com oder Ted.Neward@neudesic.com Wenn Sie interessiert ihn zusammen mit Ihrem Team, oder lesen Sie seinen Blog unter blogs.tedneward.com.

Dank der folgenden technischen Experten für die Überprüfung dieses Artikels: Luke Hoban