In diesem Beitrag möchte ich über einige beliebte Sprachen und Technologien sprechen, die Elemente der deklarativen Programmierung enthalten - PL / SQL , MS LINQ und GraphQL . Ich werde versuchen herauszufinden, welche Aufgaben in ihnen durch deklarative Programmierung gelöst werden, wie eng die deklarativen und imperativen Ansätze miteinander verflochten sind, welche Vorteile sie bieten und welche Ideen daraus gelernt werden können.
SQL-Prozedurerweiterungen
Beginnen wir mit einem Bereich, in dem dieser Verband längst zum Industriestandard geworden ist - Datenzugriffssprachen. Das bekannteste davon ist PL / SQL, eine prozedurale Erweiterung der SQL-Sprache. Mit dieser Sprache können Sie Daten in einer relationalen Datenbank sowohl mit imperativen (Variablen, Steueranweisungen, Funktionen, Objekte) als auch mit deklarativen Programmierstilen (SQL-Ausdrücke) verarbeiten. Mithilfe einer SQL-Abfrage können wir beschreiben, welche Eigenschaften die benötigten Daten haben - welche Felder benötigt werden, aus welchen Tabellen sie entnommen werden sollen, wie sie miteinander in Beziehung stehen, welche Einschränkungen sie erfüllen müssen, wie sie aggregiert werden müssen usw. Der Datenbankserver erstellt unabhängig einen Ausführungsplan für Abfragen und findet alle möglichen Feldsätze, die die angegebenen Bedingungen erfüllen. Mit dem prozeduralen Teil von PL / SQL können Sie diese Aufgaben implementierendie sich nur schwer oder gar nicht deklarativ ausdrücken lassen - das Ergebnis einer Abfrage in einer Schleife verarbeiten, beliebige Berechnungen durchführen, den Code in Funktionen und Klassen strukturieren.
Die prozeduralen und deklarativen Komponenten der Sprache sind eng miteinander verbunden. Mit PL / SQL können Sie Funktionen deklarieren, Abfragen in ihnen ausführen und ihre Ergebnisse zurückgeben, Funktionen in einer Abfrage verwenden und ihnen die Werte von Tabellenfeldern als Argumente übergeben. Sie können mit Cursorn auf die Ergebnisse einer Abfrage zugreifen und dann unbedingt alle empfangenen Datensätze durchlaufen. Cursor geben Ihnen mehr Kontrolle über den Inhalt von Tabellen und ermöglichen Ihnen die Implementierung einer viel komplexeren Datenverarbeitungslogik als die alleinige Verwendung von SQL. Ein Cursor kann einer Cursor-Variablen zugewiesen und als Argument an Funktionen, Prozeduren oder sogar eine Client-Anwendung übergeben werden. Der Anforderungscode selbst kann dynamisch durch eine Folge von imperativen Befehlen generiert werden.Durch die Kombination von Prozeduren und Abfragen mit einigen Optimierungen können Sie rekursive Abfragen implementieren. Es gibt sogar objektorientierte Funktionen in PL / SQL, mit denen Sie zusammengesetzte Datentypen für Tabellenfelder deklarieren, Methoden in sie aufnehmen und Klassen durch Vererbung erstellen können.
Mit PL / SQL können Sie Geschäftslogik auf der Datenbankserverseite implementieren. Darüber hinaus wird die Implementierung des Domänenmodells seiner Beschreibung ziemlich nahe kommen. Die Grundkonzepte des Domänenmodells werden dem relationalen Datenmodell zugeordnet. Die Konzepte entsprechen Tabellen, Attributen - ihren Feldern. Einschränkungen für Feldwerte können in Tabellenbeschreibungen eingebettet werden. Beziehungen zu anderen Tabellen können mithilfe von Fremdschlüsseln festgelegt werden. Die abstrakten Konzepte, die auf der Basis der Basiskonzepte konstruiert wurden, entsprechen der Ansicht. Sie können in Abfragen zusammen mit Tabellen verwendet werden, auch zum Erstellen anderer Ansichten. Ansichten werden auf der Grundlage von Abfragen erstellt, sodass Sie die volle Leistung und Flexibilität von SQL nutzen können. Auf diese Weise,Aus Tabellen und Ansichten können Sie ein ziemlich komplexes und mehrstufiges Domänenmodell vollständig in einem deklarativen Stil erstellen. Und alles, was nicht gut in den deklarativen Stil passt, kann mithilfe von Prozeduren und Funktionen implementiert werden.
Das Hauptproblem besteht darin, dass PL / SQL-Code ausschließlich auf der DB-Serverseite ausgeführt wird. Dies macht es schwierig, eine solche Lösung zu skalieren. Darüber hinaus wird das resultierende Modell fest an die relationale Datenbank gebunden sein und es wird problematisch sein, Daten aus anderen Quellen darin aufzunehmen.
Sprachintegrierte Abfrage
Language Integrated Query (LINQ) ist eine beliebte Komponente der .NET-Plattform, mit der Sie SQL-Abfrageausdrücke auf natürliche Weise in Ihren objektorientierten Hauptsprachencode aufnehmen können. Im Gegensatz zu PL / SQL, das SQL auf der Datenbankserverseite ein zwingendes Paradigma hinzufügt, bringt LINQ SQL auf Anwendungsebene. Aus diesem Grund können LINQ-Abfragen verwendet werden, um Daten nicht nur aus relationalen Datenbanken, sondern auch aus Sammlungen von Objekten, XML-Dokumenten und anderen LINQ-Abfragen abzurufen.
Die LINQ-Architektur ist sehr flexibel und die Abfragedefinitionen sind tief in das OOP-Modell integriert. Mit LINQ können Sie Ihre eigenen Anbieter erstellen, um auf neue Datenquellen zuzugreifen. Sie können auch Ihre eigene Art der Ausführung der Abfrage festlegen und beispielsweise den LINQ-Ausdrucksbaum der Abfrage in eine Abfrage in die gewünschte Datenquelle konvertieren. Sie können Lambda-Ausdrücke und -Funktionen verwenden, die in Ihrem Anwendungscode in Ihrem Anforderungshauptteil definiert sind. Richtig, im Fall von LINQ to SQL wird die Abfrage auf der Datenbankserverseite ausgeführt, wo diese Funktionen nicht verfügbar sind, aber stattdessen gespeicherte Prozeduren verwendet werden können. Die Anfrage ist die Essenz der Sprache der ersten Ebene. Sie können damit wie mit einem normalen Objekt arbeiten. Der Compiler kann automatisch auf den Typ des Abfrageergebnisses schließen und die entsprechende Klasse generieren, auch wenn diese nicht explizit deklariert wurde.
Versuchen wir, mit LINQ ein Domänenmodell als eine Reihe von Abfragen zu erstellen. Erste Fakten können in Listen auf der Anwendungsseite oder in Tabellen auf der Datenbankseite platziert werden, und abstrakte Konzepte können als LINQ-Abfragen formatiert werden. Mit LINQ können Sie Abfragen basierend auf anderen Abfragen erstellen, indem Sie sie in der FROM-Klausel angeben. Auf diese Weise können Sie ein neues Konzept basierend auf vorhandenen erstellen. Die Felder im Abschnitt SELECT entsprechen den Attributen des Konzepts. Der Abschnitt WHERE enthält Abhängigkeiten zwischen Konzepten. Ein Beispiel mit Rechnungen aus einer früheren Veröffentlichung sieht folgendermaßen aus.
Wir werden Objekte mit Konten und Kundeninformationen in die Listen aufnehmen:
List<Bill> bills = new List<Bill>() { ... };
List<Client> clients = new List<Client>() { ... };
Und dann erstellen wir Abfragen für sie, um unbezahlte Rechnungen und Schuldner zu erhalten:
IEnumerable<Bill> unpaidBillsQuery =
from bill in bills
where bill.AmountToPay > bill.AmountPaid
select bill;
IEnumerable<Client> debtorsQuery =
from bill in unpaidBillsQuery
join client in clients on bill.ClientId equals client.ClientId
select client;
Das mit LINQ implementierte Domänenmodell hat eine ziemlich bizarre Form angenommen - es sind mehrere Programmierstile gleichzeitig beteiligt. Die oberste Ebene des Modells weist eine zwingende Semantik auf. Es kann als Ketten von Objekttransformationen dargestellt werden, die Sammlungen von Objekten auf Sammlungen aufbauen. Abfrageobjekte sind Elemente der OOP-Welt. Sie müssen erstellt, Variablen zugewiesen und Verweise auf sie an andere Anforderungen übergeben werden. Auf der mittleren Ebene implementiert das Abfrageobjekt die Prozedur zum Ausführen einer Abfrage, die funktional mit Lambda-Ausdrücken angepasst ist, mit denen Sie die Ergebnisstruktur im Abschnitt SELECT bilden und Datensätze in der WHERE-Klausel filtern können. Die interne Ebene wird durch eine Abfrageausführungsprozedur dargestellt, die eine logische Semantik aufweist und auf relationaler Algebra basiert.
Obwohl LINQ die Beschreibung des Domänenmodells ermöglichte, zielt die SQL-Syntax hauptsächlich auf das Abrufen und Bearbeiten von Daten ab. Es fehlen einige Konstrukte, die bei der Modellierung nützlich wären. Wenn in PL / SQL die Struktur grundlegender Konzepte sehr deutlich in Form von Tabellen und Ansichten dargestellt wurde, stellte sich heraus, dass sie in LINQ in OOP-Code gerendert wurde. Während Tabellen und Ansichten namentlich referenziert werden können, können LINQ-Abfragen in einem imperativen Stil referenziert werden. Darüber hinaus ist SQL durch das relationale Modell begrenzt und verfügt nur über eingeschränkte Funktionen, wenn mit Strukturen in Form von Diagrammen oder Bäumen gearbeitet wird.
Parallelen zwischen dem relationalen Modell und der Logikprogrammierung
Sie können sehen, dass die SQL- und Prolog-Implementierungen des Modells Ähnlichkeiten aufweisen. In SQL erstellen wir eine Ansicht basierend auf Tabellen oder anderen Ansichten, und in Prolog erstellen wir Regeln basierend auf Fakten und Regeln. In SQL sind Tabellen eine Sammlung von Feldern, und Prädikate in Prolog sind eine Sammlung von Attributen. In SQL geben wir Abhängigkeiten zwischen Tabellenfeldern als Ausdrücke in der WHERE-Klausel und in Prolog unter Verwendung von Prädikaten und booleschen Variablen an, die Prädikatattribute miteinander verknüpfen. In beiden Fällen legen wir die Lösungsspezifikation deklarativ fest, und die integrierte Abfrageausführungsengine gibt die gefundenen Datensätze in SQL oder mögliche Werte von Variablen in Prolog an uns zurück.
Diese Ähnlichkeit ist nicht zufällig. Die theoretische Grundlage der SQL-relationalen Algebra wurde zwar parallel zur Logikprogrammierung entwickelt, später wurde jedoch ein theoretischer Zusammenhang zwischen ihnen aufgedeckt. Sie haben eine gemeinsame mathematische Grundlage - Logik erster Ordnung. Das relationale Datenmodell beschreibt die Regeln für den Aufbau von Beziehungen zwischen Datentabellen und die logische Programmierung - zwischen Anweisungen. Beide Theorien verwenden unterschiedliche Begriffe, werden in unterschiedlichen Bereichen angewendet, wurden parallel entwickelt, hatten aber eine gemeinsame mathematische Grundlage.
Genau genommen ist der relationale Kalkül eine Anpassung der Logik erster Ordnung an die Arbeit mit tabellarischen Daten. Diese Frage wird hier ausführlicher erörtert .... Das heißt, jeder Ausdruck der relationalen Algebra (jede SQL-Abfrage) kann in einen Ausdruck der Logik erster Ordnung umformuliert und dann in Prolog implementiert werden. Aber nicht umgekehrt. Der relationale Kalkül ist eine Teilmenge der Logik erster Ordnung. Dies bedeutet, dass wir für einige Arten von Aussagen, die in der Logik erster Ordnung zulässig sind, keine Analogien in der relationalen Algebra finden können. Beispielsweise sind die Funktionen rekursiver Abfragen in SQL sehr begrenzt, und der Aufbau transitiver Beziehungen ist auch nicht immer verfügbar. Prolog-Operationen wie Zieldisjunktion und Negation wie Ablehnung sind in SQL viel schwieriger zu implementieren. Die flexible Syntax von Prolog bietet Ihnen mehr Flexibilität beim Arbeiten mit komplexen verschachtelten Strukturen und unterstützt Mustervergleichsoperationen.Dies macht es bequem, wenn Sie mit komplexen Datenstrukturen wie Bäumen und Grafiken arbeiten.
Aber du musst für alles bezahlen. Integrierte Abfrageausführungsalgorithmen in relationalen Datenbanken sind einfacher und weniger vielseitig als Inferenzalgorithmen in Prolog. Dies ermöglicht es, sie zu optimieren und eine viel höhere Leistung zu erzielen. Prolog ist auch nicht in der Lage, Millionen von Zeilen in relationalen Datenbanken schnell zu verarbeiten. Darüber hinaus garantiert der Inferenzalgorithmus von Prolog das Ende der Programmausführung überhaupt nicht - die Ausgabe einiger Anweisungen kann zu einer unendlichen Rekursion führen.
Übrigens gibt es an der Schnittstelle von Datenbanken und logischer Programmierung auch Technologien wie deduktive Datenbanken und die Sprache der Regeln und Abfragen an Datalog. Deduktive Datenbanken speichern anstelle von Datensätzen in Tabellen große Mengen an Fakten und Regeln in einem logischen Stil. Und Datalog sieht aus wie Prolog, konzentriert sich jedoch darauf, mit Fakten zu arbeiten, die zu Mengen zusammengefasst sind, nicht mit einzelnen Fakten. Darüber hinaus wurden einige Merkmale der darin enthaltenen Logik erster Ordnung entfernt, um den Inferenzalgorithmus für schnelles Arbeiten mit großen Datenmengen zu optimieren. Die weniger ausdrucksstarke Syntax einer logischen Sprache hat also auch ihre Vorteile.
Deklarativer Ansatz zur Beschreibung der API-Schicht
SQL bindet die Modellbildung an die Datenzugriffsschicht. Die deklarative Programmierung entwickelt sich jedoch aktiv am anderen Ende der Anwendung - in der API-Schicht. Seine Besonderheit ist, dass Informationen über die Struktur von Anforderungen für diejenigen verfügbar sein sollten, die diese API verwenden. Eine formale Beschreibung der Struktur von Anfragen und Antworten ist eine gute Form. Dementsprechend besteht der Wunsch, diese Beschreibung mit dem Anwendungscode zu synchronisieren, beispielsweise Anforderungs- und Antwortklassen basierend darauf zu generieren. In die müssen Sie dann die Logik für die Verarbeitung von Anforderungen schreiben.
GraphQL ist ein Framework zum Erstellen von APIs, das weit über diesen traditionellen Ansatz hinausgeht und nicht nur eine Abfragesprache, sondern auch eine Abfrageausführungsumgebung bietet. Es ist nicht erforderlich, Code zu generieren. Die Laufzeit versteht die Anforderungsbeschreibungen trotzdem. Um die API mit GraphQL zu implementieren, benötigen Sie:
- Beschreiben der Datentypen (Objekte) der Anwendung, die Teil der Anforderungen und Antworten sind.
- die Struktur von Anfragen und Antworten beschreiben;
- Implementieren Sie Funktionen, die die Logik zum Erstellen von Objekten implementieren, um die Werte ihrer Felder zu erhalten.
Datentypen sind Beschreibungen von Objektfeldern. Es werden Typen wie Skalartypen, Listen, Aufzählungen und Verweise auf verschachtelte Typen unterstützt. Da Typfelder Verweise auf andere Typen enthalten können, kann das gesamte Datenschema als Diagramm dargestellt werden. Die Anforderung ist eine Beschreibung der von der API angeforderten Datenstruktur. Die Beschreibung der Anforderung enthält eine Liste der erforderlichen Objekte, ihrer Felder und Eingabeattribute. Jeder Datentyp und jedes seiner Felder muss einer Resolverfunktion zugeordnet sein. Der Typ- (Objekt-) Resolver beschreibt den Algorithmus zum Abrufen seiner Objekte, der Feldresolver beschreibt die Objektfeldwerte. Sie repräsentieren Funktionen in einer der funktionalen oder objektorientierten Sprachen. Die GraphQL-Laufzeit empfängt eine Anforderung, ermittelt die erforderlichen Datentypen, ruft ihre Resolver auf, auch entlang einer Kette verschachtelter Objekte.sammelt ein Antwortobjekt.
GraphQL kombiniert die Beschreibung des deklarativen Datenschemas mit imperativen oder funktionalen Algorithmen, um diese zu erhalten. Das Datenschema wird explizit beschrieben und ist für die Anwendung von zentraler Bedeutung. Viele Leute weisen darauf hin, dass es empfehlenswert ist, ein Datenschema zu erstellen, das keine Datenquellenschemata dupliziert, sondern dem Domänenmodell entspricht. Dies macht GraphQL zu einer sehr beliebten Lösung für die Integration unterschiedlicher Datenquellen.
Mit der GraphQL-Sprache können Sie das Domänenmodell also ziemlich klar ausdrücken, es vom Rest des Codes unterscheiden und das Modell und seine Implementierung näher zusammenbringen. Leider beschränkt sich die deklarative Komponente der Sprache nur auf die Beschreibung der Zusammensetzung der Datentypen, alle anderen Beziehungen zwischen den Elementen des Modells müssen mithilfe von Resolvern implementiert werden. Einerseits ermöglichen Resolver einem Entwickler, unabhängig jede Methode zum Abrufen von Daten für ein Objekt und jede Beziehung zwischen ihnen zu implementieren. Auf der anderen Seite müssen Sie jedoch versuchen, komplexere Abfrageoptionen zu implementieren, als beispielsweise den Zugriff auf einen Datensatz per Schlüssel. Einerseits zeigt das Datenschema in GraphQL deutlich die Beziehung zwischen der API-Schicht und der Datenzugriffsschicht. Andererseits ist die führende Schicht, an die das Datenschema gebunden ist, die API-Schicht.Der Inhalt des Datenschemas wird angepasst, es enthält keine Entitäten, die nicht an der Verarbeitung von Anforderungen beteiligt sind. Obwohl die Ausdruckskraft der Datenbeschreibungssprache GraphQL solchen vollwertigen deklarativen Sprachen wie SQL und Prolog unterlegen ist, zeigt die Popularität dieses Frameworks, dass Werkzeuge zur deklarativen Beschreibung des Modells Teil moderner Programmiersprachen sein können und sollten.
Ich werde zusammenfassen
PL / SQL ist eine Sprache, die sowohl zur Beschreibung eines Domänenmodells in Form von Tabellen und Ansichten als auch zur Logik der Arbeit damit geeignet ist. Die deklarativen und prozeduralen Komponenten sind eng integriert und ergänzen sich. Das Hauptproblem besteht darin, dass diese Sprache eng mit dem Datenspeicherort verknüpft ist, nur auf der Seite des Datenbankservers ausgeführt werden kann und die Abfrageausführungslogik auf das relationale Datenmodell beschränkt ist.
Auf der Anwendungsseite können Sie Technologien wie LINQ und GraphQL verwenden, um das Modell in deklarativer Form zu beschreiben. Mit dem GraphQL-Datenschema können Sie die Struktur des Domänenmodells und die Verschachtelung seiner Konzepte klar und sehr klar beschreiben. Und die Laufzeit kann die erforderlichen Objekte automatisch erfassen. Leider müssen alle anderen Beziehungen und Verbindungen zwischen Konzepten mit Ausnahme ihrer Verschachtelung in der Schicht der Resolverfunktionen implementiert werden. LINQ hat entgegengesetzte Vor- und Nachteile. Die flexible SQL-Syntax bietet Ihnen mehr Flexibilität bei der Beschreibung der Beziehungen zwischen Konzepten. Außerhalb der Anfrage endet die Deklarativität, Anforderungsobjekte sind Elemente der OOP-Welt. Sie müssen erstellt, Variablen zugewiesen und in einem imperativen Stil verwendet werden.
Ich möchte die Vorteile von LINQ und GraphQL kombinieren. Damit war die Beschreibung der Struktur von Konzepten wie in GraphQL klar und die Beziehungen zwischen ihnen konnten wie in SQL logisch festgelegt werden. Und damit Definitionen von Konzepten direkt namentlich als Klassen verfügbar sind, ohne dass ihre Objekte explizit erstellt, Variablen zugewiesen, Verweise auf sie übergeben werden usw.
Ich werde eine solche Lösung entwerfen, indem ich eine Sprache zur Beschreibung eines Domänenmodells entwickle. Dazu ist jedoch ein Überblick über die vorhandenen Sprachen der Wissensrepräsentation erforderlich. Daher möchte ich in der nächsten Veröffentlichung über Logikprogrammierung, RDF, OWL und Rahmenlogik sprechen, sie vergleichen und versuchen, solche Funktionen zu finden, die für die entworfene Sprache zur Beschreibung der Geschäftslogik interessant wären.
Für diejenigen, die nicht auf die Veröffentlichung aller Veröffentlichungen zu Habré warten möchten, gibt es einen vollständigen Text in wissenschaftlichem Stil in englischer Sprache unter dem Link: Hybrid Ontology-Oriented Programming for Semi-Structured Data Processing .
Links zu früheren Veröffentlichungen:
Entwerfen einer Programmiersprache mit mehreren Paradigmen. Teil 1 - Wofür ist es?