JoeCaml OCaml Konzepte: Weiterführende Konzepte - OO- Modulprogrammierung am Beispiel ADT

[weiter]

OO- Modulprogrammierung am Beispiel ADT

Module bieten hohe Datenabstraktion und Kapselung, Typsicherheit in hohem Maße, und Parametrisierung in großem Umfang.
Im Gegensatz dazu stellt das Objektsystem von OCaml Vererbung/Mehrfachvererbung, die Dynamische Bindung und eine Parametrisierung in kleinerem Umfang zur Verfügung.
Abstrakte Datentypen können somit in beiden Systemen jeweils eigenständig implementiert werden oder aber auch in Kombination.
Die Möglichkeiten der Umsetzung sind sehr vielfältig, was dem Programmierer eine große Freiheit in der Implementierung bietet.

Aufgabe:

ADT für geordnete Elemente (z.B. Integers, Strings, ...)

Implementierung:

An einem etwas umfangreicheren Beispiel eines abstrakten Datentyps für geordnete Elemente (hier konkret als geordnete Integer und Strings implementiert) soll abschließend die Unterschiede aber auch Gemeinsamkeiten bei der Modul- und objektorientierten Programmierung aufgezeigt werden, sowie die Möglichkeit diese zu kombinieren.
Der abstrakte Datentyp soll dabei eine Vergleichsfunktion beinhalten welche eine totale Ordnung auf den Elementen angibt und als Wert den positiven oder negativen Abstand in der Reihenfolge der jeweiligen Elemente. Zum Beispiel soll ein Ergebnis von -3 bedeuten das erste Element verglichen mit dem Zweiten in der Reihenfolge 3 Positionen vor diesem steht.

Aus Gründen derr Übersichtlichkeit wurde in den folgenden Codeschnipseln auf den konkreten Programmcode bei den geordneten Strings verzichtet, da er nicht weiter zum Verständnis beiträgt und die Beispiele nur unnötig aufblähen würde.


[weiter]

Beispiel:
Modul

# module type ORDER =
sig
type element
val cmp : element -> element -> int
end;;

# module Ordered_Integers : ORDER =
struct
type element = int
let cmp x y = x - y
end;;

# module Ordered_Strings : ORDER =
struct
type element = string
let cmp s1 s2 = (* Alphabetische Ordnung *)
end;;

Das erste Beispiel zeigt eine rein modulare Implementation eines solchen abstrakten Datentyps.
Die Modulstrukturen Ordered_integers und Ordered_strings sind dabei isomorph, d.h. strukturgleich aber inkompatibel, da die zwei Module mit der selben Signatur Order unterschiedliche Datentypen element besitzen. Der Typ element ist abstrakt aber nicht polymorph !
In diesem relativ kleinen Beispiel wurde die Sichtbarkeit, d.h. der Zugriff durch die Schnittstelle (Signatur) nicht weiter eingeschränkt.
Desweiteren ist die explizite Festlegung der jeweiligen Datentypen int bzw. string noch etwas störend wodurch der ADT nicht wirklich von der konkreten Repräsentation abstrahiert.


[weiter]

Beispiel:
Funktor

# module MakeOrdered (Ord : ORDER):
ORDER with type element = Ord.element =
struct
type element = Ord.element
let cmp e1 e2 = Ord.cmp e1 e2
end;;

Eine Verbesserung des Abstrahierungsgrades kann die Verwendung eines Funktors sein, also einer Funktion die ein Modul auf ein Modul abbildet.
Wegen des bereits oben beschriebenen Problems der Inkompatibilität der zwei Module (Ordered_integers und Ordered_strings) ist hierbei ein sog. Sharing-Constraint (with type) erforderlich, damit sich das als Parameter fungierende, übergebene Argumentmodul und der Funktor den selben Typen element teilen.
In diesem kleinen Beispiel vieleicht nicht ganz so deutlich ersichtlich, stellt die Möglichkeit Module mittels Modulen zu parametrisieren eine wesentliche Verbesserung der Datenstrukturierung dar.
Vorstellbar, als sinnvollere Anwendung eines Funktors, wäre beispielsweise ein Modul, welches eine Menge (geordneter Elemente) mit zugehörigen Operationen kapselt und durch ein Modul parametrisiert wird, welches den Typ der Mengenelemente und beispielsweise eine Sortierfunktion auf diesen Elementen kapselt.


[weiter]

Zum Vergleich mit den folgenden objektorientierten Implementierungen in Ocaml ein entsprechender ADT in Java.

Beispiel:
Klassen in Java

interface ORDER {
interface Element {}
int cmp (Order.element e1, Order.element e2);
}

class OrderedIntegers implements ORDER {
class OrderedInteger implements ORDER.Element {
int value ;
OrderedInteger (int v) { value = v; }
}
public int cmp (ORDER.Element i, ORDER.Element j) {
return ((ORDER.Element)i).value - ((ORDER.Element)j).value;
}
}

Man erkennt in diesem Beispiel eine wesentlich höhere Flexibilität gegenüber den bisherigen modularen Implementationen in OCaml.
Diese geht allerdings auf Kosten der Typsicherheit. Am deutlichsten zeigt sich dieses an der Methode cmp.
Diese ist typunsicher, da jede Instanz des Typs Order.Element (dieses Interface also implementiert) akzeptiert wird.
Ferner ist immer wiederkehrendes Casting erforderlich (hier: Down-Casting).


[weiter]

Beispiel:
Generische Klassen

# class virtual ['element] order =
object
method virtual cmp : 'element -> 'element -> int
end;;

# class ordered_integers =
object
inherit [int] order
method cmp x y = x - y
end;;

# module Ordered_Strings =
struct
type element = string
class ordered_strings =
object
inherit [element] order
method cmp x y = (* Alphabetische Ordnung *)
end;;
end;;

Eine sichere Typüberprüfung kann realisiert werden durch eine Konstruktion mit Typparametern.
Dieser polymorphe Mechanismus wird unter anderem auch von C++ in Form von Templates und in Eiffel durch generic classes unterstützt.
OCaml bietet desweiteren die Möglichkeit der Kombination von Klassen und Modulen.

Ein Objekt kann beispielsweise in ein Modul eingebettet werden und erlaubt eine vollständige Abstraktion von der konkreten Repräsentation des abstrakten Datentypen in obigem Beispiel. Statt bei der Ableitung der Abstrakten Klasse order den Typparameter explizit (hartcodiert) zu übergeben wird dieser an einer zentralen Stelle innerhalb eines Moduls gekapselt definiert.


Beispiel:
Generische Klassen mit automatischer Instanzierung

# class virtual comparable =
object( _ : 'element )
method virtual cmp : 'element -> int
end;;

# class ordered_integers x =
object(self)
inherit comparable
val int_value = x
method get_val = int_value
method cmp i = self#get_val - i#get_val
end;;

# class ordered_strings x =
object(self)
inherit comparable
val string_value = x
method get_val = string_value
method cmp s = (* Alphabetische Ordnung *)
end;;

In diesem letzten Beispiel sind nun zwei wesentliche Verbesserungen des vorigen Beispiels zu erkennen.
Zum einen abstrahieren die konkreten Implementationen (ordered_integers und ordered_strings) von einem direkten Initialisierungswert, indem dieser in Form eines Parameters (x) dem Objekt bei einer Instanzierung übergeben wird.
Dieser Mechanismus ist vergleichbar mit der Deklaration eines Konstruktors wie beispielsweise in Java.
Durch das Fehlen der Möglichkeit Overloading zu nutzen ist jedoch ein solches Objekt auf einen einzigen es erzeugenden Konstruktors beschränkt. Lediglich die Art der Initialisierungsparameter ist variabel, die Anzahl jedoch durch eine solche Deklaration festgelegt.
Zum anderen wird die Möglichkeit der Selbstreferenzierung eines Objektes eingeführt, durch Vereinbarung einer Variablen, welche an das Objekt gebunden wird.
Diese Selbstreferenz wird nun zur Parametrisierung des jeweiligen Objekts benutzt, indem der jeweilige Typ automatisch abgeleitet wird.
Der OCaml-Objektlayer unterstützt durch Berechnung mittels eines Inferenzmechanismus diese automatische Instanzierung von Typparametern.



Hinweis:

"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.



[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Quellen und weiterführende Literatur ]