Generische Module wurden zunächst bei CLU eingeführt und etwas später in sehr ähnlicher Form in Ada. Wie Bertrand Meyer gezeigt hat, lassen sich generische Module durch die Anwendung objekt-orientierter Techniken ersetzen, jedoch zumindest im Falle von Eiffel in Verbindung mit einigen Umständlichkeiten. Aus diesem Grund wurden in etwas limitierter Form generische Module sowohl in Eiffel, Modula-3 als auch C++ (hier nennen sie sich templates) eingeführt.
Natürlich handelt es sich bei den Ersatzmechanismen nicht mehr um parametrisierte Module im strengen Sinne. Interessant ist jedoch, ob die Probleme, die zur Einführung generischer Module geführt haben, sich nicht auch in anderer Form elegant lösen lassen. Das klassische Beispiel für generische Module ist Verwaltung von Datenstrukturen, bei denen der Basistyp von untergeordneter Bedeutung ist. Folgendes einfach gehaltene Beispiel zeigt ein allgemeines Modul für Listen in Oberon. Es verwendet als Basistyp Objects.Object, das eine ähnliche Funktion wie ANY in Eiffel übernimmt, jedoch nicht Bestandteil der Sprache Oberon ist.
DEFINITION Lists; IMPORT Objects; TYPE List = POINTER TO ListRec; ListRec = RECORD (Objects.ObjectRec) END; PROCEDURE Create(VAR list: List); PROCEDURE Append(list: List; object: Objects.Object); PROCEDURE First(list: List); PROCEDURE Next(list: List; VAR object: Objects.Object) : BOOLEAN; END Lists.
Create legt eine neue Liste an. Mit Append kann ein neues Element an das Ende der Liste angehängt werden. First und Next erlauben es, die Liste zu durchlaufen. Next liefert dabei FALSE zurück, wenn kein Objekt mehr zurückgeliefert werden kann, da das Ende der Liste erreicht worden ist. In der angebotenen Form ist dies etwas unzureichend, da nur ein Durchlauf zu einem gegebenen Zeitpunkt möglich ist. Später werden Iteratoren in allgemeinerer Form vorgestellt. Die Implementierung könnte folgendermaßen aussehen:
MODULE Lists; IMPORT Objects; TYPE Linkable = POINTER TO LinkableRec; LinkableRec = RECORD next: Linkable; object: Objects.Object; END; List = POINTER TO ListRec; ListRec = RECORD (Objects.ObjectRec) (* private Komponenten *) head, tail: Linkable; cursor: Linkable; (* fuer List und Next *) END; PROCEDURE Create(VAR list: List); BEGIN NEW(list); list.head := NIL; list.tail := NIL; list.cursor := NIL; END Create; PROCEDURE Append(list: List; object: Objects.Object); VAR linkable: Linkable; BEGIN NEW(linkable); linkable.object := object; linkable.next := NIL; IF list.head = NIL THEN list.head := linkable; ELSE list.tail.next := linkable; END; list.tail := linkable; END Append; PROCEDURE First(list: List); BEGIN list.cursor := list.head; END First; PROCEDURE Next(list: List; VAR object: Objects.Object) : BOOLEAN; BEGIN IF list.cursor = NIL THEN RETURN FALSE ELSE object := list.cursor.object; list.cursor := list.cursor.next; RETURN TRUE END; END Next; END Lists.
Wie sich aus dem Beispiel ersehen läßt, erweist es sich als recht vorteilhaft, mehrere Datentypen in einem Modul vereinbaren zu können. Während bei Eiffel für Linkable eine eigene Klasse mit den zugehörigen Zugriffsoperationen kreiert werden müßte, erlaubt es Oberon, diesen Typ völlig privat zu halten.
Folgendes Beispiel demonstriert, wie auf der Basis von Lists eine Liste von Textzeilen aufgebaut und durchlaufen werden kann:
TYPE Line = ARRAY 80 OF CHAR; LineObject = POINTER TO LineObjectRec; LineObjectRec = RECORD (Objects.ObjectRec) line: Line END; PROCEDURE ReadLines(s: Streams.Stream; VAR list: Lists.List); (* Lies von dem gegebenen Stream bis zum Auftreten einer leeren Zeile oder Eingabeende und lege alle eingelesenen Zeilen in der neu zu kreierenden Liste ab *) VAR line: Line; lineObject: LineObject; BEGIN Lists.Create(list); Read.LineS(s, line); WHILE (s.count $>$ 0) \& (line \# "") DO NEW(lineObject); lineObject.line := line; Lists.Append(list, lineObject); Read.LineS(s, line); END; END ReadLines; PROCEDURE WriteLines(s: Streams.Stream; list: Lists.List); (* Gib alle Zeilen aus, die in der Liste enthalten sind; andere Objekte aus der Liste werden ignoriert *) VAR object: Objects.Object; BEGIN Lists.First(list); WHILE Lists.Next(list, object) DO IF object IS LineObject THEN Write.LineS(s, object(LineObject).line); END; END; END WriteLines;
Dieses Beispiel verdeutlicht den Unterschied zu den generischen Modulen aus CLU und Ada. Während bei CLU und Ada der Elementtyp der Liste genau festgelegt werden würde und damit statisch überprüfbar wäre, liegt hier nur fest, daß alle Elementtypen Erweiterungen von Objects.Object sein müssen. Dies liefert sowohl die Freiheit, heterogene Objekte in einer Liste zu verwalten, als auch die offene Frage, welche Sorten von Objekten man später darin wiederfindet.
Eiffel, Modula-2 und C++ legen natürlich den Elementtyp nicht so fest wie CLU und Ada, da sie auch Erweiterungen akzeptieren. Im Gegensatz zur obigen Lösung liefern sie vollständige statische Überprüfbarkeit auf Kosten eines nicht trivialen Sprachmittels. Allerdings ist dies bei Eiffel nicht vollständig gelungen: So zeigt Cook neben einigen Problemen im Typsystem von Eiffel auch eines in Zusammenhang mit generischen Klassen auf, das - wenn überhaupt - nur zur Laufzeit entdeckt werden kann.
Wenn im obigen Beispiel WriteLines absolut sichergehen kann, daß alle Objekte der Liste Erweiterungen von LineObject sind, dann kann der Programmtext weiter vereinfacht werden:
PROCEDURE WriteLines(s: Streams.Stream; list: Lists.List); (* Gib alle Zeilen aus, die in der Liste enthalten sind; es wird davon ausgegangen, dass alle Elemente der Liste Erweiterungen von LineObject sind *) VAR lineObject: LineObject; BEGIN Lists.First(list); WHILE Lists.Next(list, lineObject) DO Write.LineS(s, lineObject.line); END; END WriteLines;
Dies ist erlaubt, da LineObject eine Erweiterung von Objects.Object ist und damit direkt an Lists.Next übergeben werden kann. In diesem Fall würde es innerhalb von Lists.Next zu einem Laufzeitfehler bei der Zuweisung führen, falls das zugewiesene Element doch keine Erweiterung von LineObject ist.
Ada und CLU offerieren zusätzlich die Möglichkeit, Prozeduren und Variablen als Parameter bei generischen Modulen anzugeben. Dies ist beispielsweise sinnvoll bei einem allgemeinen Sortiermodul, das eine Vergleichsprozedur benötigt. In Oberon kann hierfür der Basistyp so definiert werden, daß zu ihm eine Sortierprozedur gehört (Techniken dazu folgen), oder es kann mit konventioneller Technik analog zu Modula-2 ein Prozedurparameter explizit angegeben werden.