Montag, 30. Januar 2012

Von der Regel zum semantischen Modell

In diesem Blog will ich beschreiben, wie man mit wenig Aufwand einen Parser für eine selbstdefinierte domänenspezifische Sprache schreiben kann, der die vom Benutzer eingegebenen Regeln in passende ABAP-Datenstrukturen konvertiert. Ich verwende ein Beispiel, das ich hier schon öfter herangezogen habe: Die Verpackung von Lieferungen in Paletten.

Eine Palette kann den Inhalt einer oder mehrerer Lieferungen haben. Paletten können aber auch zu grösseren Verpackungseinheiten zusammengefasst werden, auch gemischt mit Lieferungen, und auch diese zusammengefasste Einheit ist wieder eine Palette. Vom Datenmodell ist eine Palette also ein Kompositum.

Wenn wir jeder Palette und jeder Lieferung eine Belegnummer geben und den Inhalt einer Palette darüberhinaus mit einer Positionsnummer durchnumerieren, können wir folgendes beispielhaftes Datenmodell in der Sprache ABAP formulieren:

types:

ty_doc_number(10) type c,
ty_item_number(6) type c,
ty_pallet_item_type(2) type c,
ty_doc_numbers type standard table of ty_doc_number
with non-unique default key,

begin of ty_pallet_item,
pallet_no type ty_doc_number,
item_no type ty_item_number,
type type ty_pallet_item_type,
content type ty_doc_number,
end of ty_pallet_item,
ty_pallets type standard table of ty_pallet_item
with key pallet_no item_no,

begin of ty_model,
pallet_numbers type ty_doc_numbers,
delivery_numbers type ty_doc_numbers,
pallets type ty_pallets,
end of ty_model.

Als mögliche Typen einer Position kommen nur "Palette" und "Lieferung" in Frage. Dafür prägen wir, wenn wir schon dabei sind, noch eine Konstante aus:
constants: begin of gc_pallet_item_type,
pallet type ty_pallet_item_type value 'PA',
delivery type ty_pallet_item_type value 'DL',
end of gc_pallet_item_type.


Eine Instanz von ty_model ist demnach ein strukturiertes Datenobjekt, das aus drei Tabellen besteht: Je eine Tabelle enthält die benötigten oder verwendeten Lieferungs- und Palettennummern, und die dritte Tabelle listet positionsweise den Inhalt jeder Palette auf.

Eine Instanz von ty_model repräsentiert somit eine Packregel. Sie gibt an, auf welche Weise ein Vorrat von n Lieferungen auf m Paletten zu verteilen bzw. in m Paletten zu organisieren ist.

Eine nach meiner Ansicht für den Benutzer angenehme Notation einer solchen Packregel würde die Paletten und Lieferungen mit Ordinalzahlen durchnumerieren und Paletteninhalte in Klammern spezifizieren. Ein einfacher Fall:
1. Palette (
1. Lieferung
2. Lieferung )

würde also vorschreiben, den Inhalt von zwei Lieferungen in einer Palette zu verpacken.

Ein wenig komplexer wäre schon der hierarchische Fall:
1. Palette (
1. Lieferung
2. Palette )
2. Palette (
2. Lieferung
3. Lieferung )

Hier kommt die 2. Palette in zwei Notationen vor: Einmal als blosse Referenz - indem angegeben ist, dass sie zum Inhalt der 1. Palette gehört - und ein weiteres Mal, um ihren Inhalt zu spezifizieren. Es soll aber auch erlaubt sein, die zweite Notation geschachtelt zu verwenden, etwa so:
1. Palette (
1. Lieferung
2. Palette (
2. Lieferung
3. Lieferung
)
)

Das ist dieselbe Verpackungsvorschrift wie die vorherige - nur anders notiert.

Zeilenvorschub soll keine andere Rolle haben als anderer Leerraum auch. Ausserdem wäre es noch schön, wenn man abgekürzte Wörter wie "Lief" und "Pal" verwenden kann, um sich Schreibarbeit zu ersparen:
1. Pal ( 1. Lief 2. Lief )

Schliesslich soll es möglich sein, nacheinander mehrere Packregeln aufzuführen.

Wie kann man nun mit möglichst wenig Aufwand einen Übersetzer schreiben, der eine solche, frei erfundene Notation in das obige ABAP-Datenmodell transformiert?

In oMeta würde man nur die folgenden acht Zeilen benötigen, um einen Parser für die oben beschriebene Syntax zu definieren:
ometa HandlingUnitDefinition <: Parser {
expr = fullPallet+ end,
fullPallet = pallet "(" content ")",
pallet = ordnum ( "Palette" | "Pal" | "SSCC" ),
content = (spaces contentPart)+,
contentPart = fullPallet | delivery | pallet,
delivery = ordnum ( "Lieferung" | "Lief" | "LF" ),
ordnum = spaces digit+ "."
}


Auf die Details der Syntax von oMeta will ich hier nicht eingehen – ich habe die wesentlichen Züge dieser Sprache bereits in meinem Blog Eine objektorientierte Metasprache beschrieben. Hier soll es um die Integration von oMeta in ABAP gehen - und darum, um welche semantischen Aktionen man diese Definition noch erweitern muss, um eine Abbildung in ABAP-Daten zu erhalten.

Da es (noch! siehe [2]) den in ABAP eingebauten JavaScript-Interpreter CL_JAVA_SCRIPT gibt, ist es auch möglich, die oMeta/JS-Implementierung in ABAP zu verwenden. Als Basis dafür habe ich den Subroutinen-Pool Z_OMETA_BASE geschrieben. Ich habe extra die Form des Subroutine-Pools gewählt, der leicht mit Copy & Paste in ein (fast) beliebiges SAP-System übernommen werden kann.

Aus dem Programm Z_SEMANTIC_MODEL_EXAMPLE, das diese Übersetzung in ABAP leistet, schauen wir uns einmal die zentrale Routine rule_to_model an, die eine als String nach obigen Regeln eingegebene Verpackungsregel in eine Instanz von ty_model konvertiert:

* --- Apply parser to input, populating the semantic model
form rule_to_model using iv_rule type string
changing es_model type ty_model
ev_parse_errors type flag.

data: lv_error_code type i,
lv_result type string.

clear es_model.

* Bind the semantic model
perform bind(z_ometa_base)
using 'pallets.model' es_model.

* Execute the parser
perform match(z_ometa_base)
using 'HandlingUnitDefinition' 'expr' iv_rule
changing lv_result lv_error_code.

if lv_error_code <> 0.
ev_parse_errors = abap_true.
message 'The rule entered is syntactically incorrect' type 'I'.
else.
ev_parse_errors = abap_false.
perform normalize_model changing es_model.
endif.

endform. "rule_to_model

Das aktuelle ABAP-Datenobjekt, das gefüllt werden soll, muss dem Interpreter bekannt gemacht werden. Durch Aufruf der bind()-Methode wird in JavaScript ein Proxy-Objekt erzeugt, auf das man mit JavaScript zugreifen kann. Ist dies gemacht, erfolgt der Aufbau der Daten durch Aufruf der Regel expr (zuvor generierten) Parsers HandlingUnitDefinition.

Mit der obigen Grammatik HandlinUnitDefinition allein wird das natürlich nicht funktionieren. Denn sie beschreibt ja nur die Syntax. Sie muss noch um die nötige Semantik angereichert werden. Das macht man in oMeta mit den sogenannten semantischen Aktionen. Wenn eine Regel passt, kann man, von einem -> gefolgt, Angaben im Quellcode der Hostsprache des Parsers (hier JavaScript) machen, die in diesem Fall auszuführen sind. Das Ergebnis der Evaluation einer solchen semantischen Aktion kann darüberhinaus in Form von Symbolen in anderen Regeln abgegriffen und in deren semantischen Aktionen weiterverwendet werden.

Hier sind nun mit den Regeln für delivery, pallet und fullPallet bestimmte JavaScript-Funktionen addDelivery(), addPallet() und buildPallet() verknüpft, in denen die gelesenen Daten direkt an die gebundenen ABAP-Variablen übertragen werden.
    ometa HandlingUnitDefinition <: Parser {
expr = fullPallet+ end,
fullPallet = pallet:s "(" content:c ")"
-> { buildPallet( s, c ) },
pallet = ordnum:n ( "Palette" | "Pal" | "SSCC" )
-> {addPallet(n)},
content = (spaces contentPart)+,
contentPart = fullPallet | delivery | pallet,
delivery = ordnum:n ( "Lieferung" | "Lief" | "LF" )
-> {addDelivery(n)},
ordnum = spaces digit+:ds "." -> parseInt(ds.join(''))
}


Die Zugriffsnotation für gebundene ABAP-Variablen ist in der ABAP-Hilfe dokumentiert. In dieser Notation müssen die Funktionen addDelivery(), addPallet() und buildPallet() folgendermassen implementiert werden.

function addDelivery(n) {
return addDocNumber(pallets.model.delivery_numbers,"DL"+n);
}
function addPallet(n) {
return addDocNumber(pallets.model.pallet_numbers,"PA"+n);
}
function buildPallet( id, content ) {
var p = pallets.model.pallets;
var wa, i;
for (i=0;i<content.length;i++) {
p.appendLine();
wa = p[p.length-1];
wa.pallet_no = id;
wa.item_no = i+1;
wa.type = content[i].substring(0,2);
wa.content = content[i];
}
return id;
}
function addDocNumber(table,id) {
table.appendLine();
table[table.length-1] = id;
return id;
}


Das ist alles. Probieren Sie es in Ihrem SAP-System aus: Sie können den Report Z_SEMANTIC_MODEL_EXAMPLE und den von diesem vorausgesetzten Unterprogrammpool Z_OMETA_BASE herunterladen und ersteren dann ausführen. Es erscheint ein Popup, in dem Sie eine stringförmige Verpackungsregel nach obiger Syntax eingeben können:[1]



Wenn die Syntax eingehalten wurde, kann die Transformation ausgeführt werden. Das von ihr produzierte semantische Modell wird schliesslich mit dem Baustein RS_COMPLEX_OBJECT_EDIT angezeigt (der auch vom SAP-Standard als Benutzerschnittstelle zur Dateneingabe beim Funktionsbaustein-Einzeltest verwendet wird):



Dieses Modell kann nun mit ABAP-Mitteln weiterverarbeitet werden. Es könnte beispielsweise als Template zum Abfüllen realer Lieferungen in Paletten benutzt werden.

Ich erwähnte im Blog Domänenspezifische Programmierung in ABAP, dass ein ähnliches Tool in meiner Firma im Einsatz ist. Ich hatte allerdings den Parser damals mit Hilfe von regulären Ausdrücken ausprogrammiert. Der Sinn dieses Blogs ist zu zeigen, dass sich das Parsen und Transformieren von Ausdrücken in einer frei definierten Syntax erheblich verkürzt - wenn man eine geeignete Metasprache verwendet.

Das muss nicht oMeta und nicht JavaScript sein. Nur reizte mich der Titel von Alessandro Warth's Dissertation über oMeta - Experimenting with Programming Languages [3] - zu einer Machbarkeitsstudie.

[1] Soweit ich das gesehen habe, gibt es in der Basis genau einen Funktionsbaustein, der ein Popup zur Eingabe eines freien Textes sendet. Er heisst TERM_CONTROL_EDIT und stammt vermutlich noch aus der Zeit, als das TextEditControl entwickelt wurde (das "Enjoy"-Release).
[2] Leider wurde die Klasse CL_JAVA_SCRIPT von SAP für Release 7.02 zum Auslaufmodell erklärt. Die Dokumentation kündigt an, die Klasse zu einem späteren Release aus der Wartung zu nehmen. Leider fällt sogar das Wort von der ersatzlosen Streichung. Das wird es für Zugänge wie den hier beschriebenen schwer machen. In Java hat man mit dem JSR-223 schon lange erkannt, wie wichtig die Integration dynamischer Sprachen ist. Mit Java SE6 kam dann ein Framework, um beliebige dynamische Sprachen in Java zu integrieren, das zu SE7 sogar noch um einige neue Bytecode-Anweisungen wie invokeDynamic bereichert wurde. Das zeigt, welch hoher Stellenwert in der Java-Welt den dynamischen Sprachen beigemessen wird.
[3] Alessandro Warth, Experimenting with Programming Languages, PhD dissertation, 2009, http://www.vpri.org/pdf/tr2008003_experimenting.pdf

Keine Kommentare :