| Diese Seminararbeit aus dem Informatik-Seminar WS2004/05:
Programmierkonzepte und Programmiersprachen wurde für interne Zwecke (Weiterbildung)
der Fa. Condor EDV von Hr. Prof. Dr. Uwe Schmidt von der University of Applied Sciences,
Wedel, Germany
bereitgestellt. Alle Rechte liegen beim bisherigen Copyright-Inhaber.
Weiternutzung in jeglicher Form, auch Auszugsweise bedürfen der
Einverständniserklärung des Copyright-Inhaber.
Alle Links in dieser lokalen Zusammenfassung sind Verweise zum Orginal. Das Orginal ist unter aufrufbar. Wir danken Hr. Prof. Uwe Schmidt für die Einverständniserklärung. | 
|  | OCaml Konzepte: | 
Gliederung
|  | OCaml Konzepte: Einleitung | 
| Geschichte von OCaml : ML wird um 1973 von Robin Milner in Edinburgh entwickelt als Programmiersprache (MetaLanguage) für den LCF Theorembeweiser. 1985 entwickelt Gérard Huet und andere CAML = Categorical Abstract Machine + ML als Implementierungssprache für den CoQ Theorembeweiser am INRIA, Frankreich. 1990 entwickelt Xavier Leroy eine neue sehr effiziente Implementierung von CAML und fügt Objektorientierung hinzu. OCaml = Objective Caml. | 
Dazu gehören:
|  | OCaml Konzepte: Gliederung | 
Grundlegende Konzepte
|  | OCaml Konzepte: Grundlegende Konzepte - Das Typensystem von OCaml | 
| Typen | Beschreibung für eine Menge von Werten | |
| Werte | Zahlen, Zeichen, Listen, Funktionen, ... | 
Die ML-Sprachfamilie ist statisch und streng getypt.
Das heißt der Typ aller Variablen und Konstanten wird vollständig und statisch zur Übersetzungszeit ermittelt.
| Automatische Typenanalyse | Typinferenz und Typsynthese | 
Unter Typinferenz versteht man die Bestimmung von Typen zu ungetypten Ausdrücken,
 jedoch sind nicht alle Ausdrücke typisierbar.
OCaml ist in der Lage, mittels des Mechanismus der automatischen Typenanalyse durch Typinferenz und Typsynthese,
 den Typ eines Ausdrucks ohne vorgegebene Typeninformation selbst herzuleiten.
Dabei geht OCaml wie folgt vor:
| Vorgehensweise | Jedem Unterausdruck wird ein Typ zugewiesen | 
Primitive Datentypen in OCaml:
| Typ | Operatoren | 
|---|---|
| unit | keine | 
| bool | not , && , || , ... | 
| int | + , * , - , / , = , <> , < , > , ... | 
| float | +. , *. , -. , /. , < , > , ... | 
| char | = , <> , < , > , ... | 
| string | = , <> , < , > , ... | 
Alle in einer modernen Hochsprache benötigten bzw. gewünschten
Datentypen stehen durch die in der Kern-Bibliothek vordefinierten Typen
zur Verfügung.
Der Typ unit entspricht dem Datentypen void in C und wird für
resultatlose Funktionen (vorwiegend Prozeduren zur E\A-Verarbeitung)
verwendet, oder dient als Platzhalter, wenn keine Werte übergeben
werden müssen.
Es existieren unterschiedliche arithmetische Operatoren jeweils für Ganzzahlarithmetik und Fließkommazahlarithmetik.
Die relationalen Operatoren (zweiargumentige vordefinierte Funktionen)
sind überwiegend polymorph, d.h. für mehrere Datentypen definiert.
Ausserdem bietet OCaml die integrierten Datentypen Array und Liste, sowie die Möglichkeit eigene Datentypen (Aggregattypen) wie Records, Unions und Tupel zu definieren.
|  | OCaml Konzepte: Grundlegende Konzepte - Die Auswertungsstrategie von OCaml | 
| Auswertung | Eager-Evaluation | |
| Prinzip | Call-By-Value | |
| Reihenfolge | Applicative Order | 
Zur Auswertung von Ausdrücken verwendet OCaml die Eager-Auswertungsstrategie. Das Prinzip des "Call-By-Value" folgt
 diesem Ansatz, d.h. alle Argumente einer Funktion werden ausgewertet bevor sie an eine Funktion übergeben werden.
Die Auswertungsreihenfolge ist Applicative Order.
Die Auswertung eines Ausdrucks kann als Transformationsprozess aufgefasst werden, der so lange fortgeführt wird,
 bis der Ausdruck nicht weiter ausgewertet werden kann.
Dabei sind folgende Regeln zu beachten:
| Regeln | 
 | |
| Ausnahme | # if expr1 then expr2 else expr3;; | 
Bei einem if-then-else-Konstrukt wird zuerst die Bedingung (expr1) ausgewertet, und dann abhängig von diesem Ergebnis entweder der Ausdruck des Then-Zweigs (expr2) oder des Else-Zweigs (expr3) ausgewertet.
Wird ein Ausdruck ausgewertet, ergeben sich vier Möglichkeiten:
Diese Auswertungsstrategie kann nachteilig sein, da häufig das Ergebnis einer Funktion nicht von allen Argumenten abhängt, jedoch immer alle Argumente ausgewertet werden.
| Beispiel: Nachteil | 
 | 
Die Funktion zero wird, wie durch die oben genannten Auswertungsregeln beschrieben, ausgewertet, indem sukzessiv die Argumente der Funktion ausgewertet werden, obwohl das Ergebnis dieser Auswertungen nicht benötigt wird und die Funktion stets das gleiche Ergebnis liefert.
Zum Problem kann diese Auswertungsstrategie werden bei der Konstruktion eigener Kontrollstrukturen.
Diese werden in funktionalen Programmiersprachen häufig verwendet. Die größte Gefahr dabei besteht in
 nichtterminierende Auswertungen durch beispielsweise Endlosrekursionen zu laufen.
| Beispiel: Problem | 
 | 
Da, wie durch die Auswertungsstrategie der Eager-Evaluation vorgesehen, mit der Auswertung der Argumente begonnen wird,
 bevor deren Wert an die Funktion weitergereicht wird, kann das Abbruchkriterium dieser Kontrollstruktur nicht greifen,
  genauergesagt kommt es erst gar nicht zu dessen Überprüfung.
Der letzte Ausdruck von Funktion 3, also der steuernde rekursive Funktionsaufruf der Funktion selbst, wird ausgewertet,
 bevor eine Verzweigung in Abhängigkeit des Bedingungstests erfolgen kann.
 Dies führt unweigerlich zu einem Stack-Overflow.
Zum Vergleich:
Haskell verwendet die Lazy-Auswertungsstrategie nach dem Prinzip des "Call-By-Need". Hierbei werden die Argumente erst zum Bedarfszeitpunkt ausgewertet. Die Auswertungsreihenfolge dabei ist Normal Order.
OCaml verwendet die Eager-Evaluation zum einen aus Effizienzüberlegungen und der absehbaren Speichernutzung von Programmen, zum anderen wegen den Schwierigkeiten, die sich aus der Möglichkeit der Nutzung von imperativen Konstrukten und Seiteneffekten ergeben.
Es besteht jedoch trotzdem die Möglichkeit, die Vorteile, welche die Lazy-Evaluation bietet, in OCaml zu nutzen.
Durch Einbindung des "Lazy-Moduls", eine der vielen verfügbaren Bibliotheken für Objective Caml,
 ist Programmierung nach dem Call-By-Need Prinzip umsetzbar.
|  | OCaml Konzepte: Grundlegende Konzepte - Variablen | 
Im Gegensatz zu imperativen Sprachen werden Variablen in funktionalen Sprachen als aktuelle Werte aufgefaßt oder dienen als definierte Bezeichner für Werte, da in der Mathematik der Begriff des Speicherplatzes nicht existiert oder von Bedeutung wäre.
| Variable | Name für einen Wert | |
| Bindung | Werte werden an Namen statisch gebunden | |
| Syntax: Einfache Bindung | # let name = expr;; | |
| Syntax: Geschachtelte Bindung | # let name = expr1 in expr2;; | 
Variablen können einfach an einen Wert gebunden werden, oder innerhalb einer geschachtelten Konstruktion.
Bei der geschachtelten Variablenbindung ist die Variable name definiert als der Wert des Ausdrucks expr1 innerhalb des Rumpfes
 der Bindung, dem Ausdruck expr2.
| Beispiel: Geschachtelte Bindung | 
 | 
Die Variable x ist demnach definiert als der ganzzahlige Wert 1 innerhalb des Rumpfes dieser Variablenbindung,
 in welchem dieser Bezeichner redefiniert wird als der Wert 2 innerhalb einer weiteren Schachtelung.
In diesem Rumpf wird ein neuer Bezeichner (die Variable y) definiert
als die Summe des Wertes "x" mit sich selbst, also y = 2 + 2 = 4.
Der Wert der Variablen y im innersten Bindungsrumpf, führt zur
Auswertung des letzlich durch x + y (2 + 4 = 6) definierten
Gesamtausdrucks.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Funktionen | 
Funktionen stellen den Hauptbestandteil aller Sprachkonstrukte funktionaler Sprachen dar.
| Funktion | Eindeutige Zuordnungsvorschrift zwischen Argumenten und Resultat | |
| Funktionen höherer Ordnung | Funktionen, die Funktionen als Parameter besitzen oder deren Wertebereich Funktionen enthält | |
| In ML-Sprachfamilie: | 
 | |
| Syntax: | # fun p1 ... pn -> expr;; | 
Im Gegensatz zu den vordefinierten Operatoren (im Prinzip auch Funktionen), welche geklammert in Prefix- oder wie gewohnt in Infix-Notation geschrieben werden, deklariert man Funktionen in OCaml nach der aus der Mathematik stammenden Notation: x -> f(x).
Funktionen nehmen Argumente und liefern ein Resultat, aber sie haben keine Nebeneffekte (lässt man die Möglichkeiten der
 imperativen Sprachkonstrukte von OCaml wie Manipulation von Datenstrukturen ect. ausser acht.).
Funktionsparameter, sowie auch Funktionsresultate sind strukturiert und typisiert.
Funktionen können selbst Argumente oder Resultate sein, sowie beliebig
innerhalb Funktionskompositionen miteinander kombiniert werden.
Auuserdem lassen sich Funktionen zu generischen Funktionen gruppieren, wenn eine Sprache wie OCaml die Möglichkeit bietet,
 Funktionen als Argumente zu übergeben.
Funktionen werden im funktionalen Sprachgebrauch auch als "First-Class-Citizens" bezeichnet, was bedeutet,
 daß Funktionen manipuliert werden können wie andere Variablen auch.
| Vergleich: Bindungsmechanismus Variablen | # let name = expr1 in expr2;; | |
| Bindungsmechanismus Funktionen | # ( fun x -> expr2 ) (expr1);; | 
Bei einem Vergleich des Bindungsmechanismus bei der Variablenbindung und Funktionsbindung ist zu erkennen, daß beide Formen nach den Auswertungsregeln äquivalent sind.
| Beispiel: Ein-Argument-Funktion | # let incr = fun i -> i + 1;; val incr : int -> int = <fun> | |
| Mehr-Argument-Funktion | # let sum = fun i j -> i + j;; val sum : int -> int -> int = <fun> | 
Der Pfeil für einen Funktionstypen ist rechts assoziativ, d.h. int -> (int -> int).
Die Funktion sum ist also eine Funktion mit einem int-Argument, die eine Funktion mit einem anderen int-Argument liefert,
 welche einen int-Wert zurückgibt.
Dies wiederum bedeutet, daß alle Mehrargumentfunktionen als geschachtelte Funktionen implementiert sind.
Dieses Prinzip nennt man Currying, nach Haskell Curry.
Da die Funktion sum eigentlich eine Funktion als Resultat liefert,
handelt es sich bei dieser Funktion bereits um eine Funktion höherer
Ordnung.
Curried-Funktionen, also alle Mehrargumentfunktionen beschreiben die
einfachste Form einer Funktion höherer Ordnung.
Die explizite Curried-Form von sum ist demnach äquivalent zu obiger Deklaration.
| Beispiel: Explizite Curried-Form | # let sum = ( fun i -> ( fun j -> i + j ));; val sum : int -> int -> int = <fun> | 
| Beispiel: Partielle Applikation | # let incr = sum 1;; # incr (incr 1);; | 
Funktionen in OCaml können auch nur partiell appliziert werden.
Eine Partielle Applikation ist eine Funktionsanwendung auf ein oder mehrere aber nicht alle Argumente einer Funktion.
Die redifinierte Fassung von incr als teilweise Anwendung von sum liefert eine Funktion, welche noch eine Ganzzahl erwartet,
 um mit der Berechnung fortzufahren. In dieser Form kann man sehr schön geschachtelte Ausdrücke formulieren,
 für fortschreitende bzw zurückschreitende Berechnungsvorschriften (z.B. pred/succ, incr/decr, ...).
| Beispiel: Funktion höherer Ordnung | # let square x = x *. x;; val square : float -> float = <fun> | |
| Rechtsassoziativität ! | 
 
 | 
Als naheliegendes Beispiel für Funktionen höherer Ordnung sei an
dieser Stelle die Ableitung einer mathematischen Funktion genannt.
Die Ableitungsfunktion (derive) bildet eine einfache Funktion (square) auf eine neue Funktion ab.
Zu beachten ist wieder die Rechtsassoziativität des Funktionstypen: (float -> float) -> (float -> (float -> float)).
Die Funktion derive ist demnach eine Funktion die eine Funktion als
Argument erwartet, die abzuleitende mathematische Funktion
(float -> float), und eine Funktion liefert die ein weiteres
Argument (delta dx) erwartet und als Resultat eine Funktion zurückgibt,
die abgeleitete mathematische Funktion.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Pattern Matching | 
Pattern Matching ist eine der Stärken der ML-Sprachfamilie und wird bei allen Bindungsmechanismen (z.B. Variablen, Funktionen) verwendet.
| Pattern | Ausdruck bestehend aus: 
 | |
| Pattern Matching | Mechanismus: 
 | |
| Syntax: | 
 | 
Die Abarbeitung eines Match-Ausdrucks erfolgt in der Reihenfolge der Deklaration, also von oben nach unten.
Mehrere Patterns können dabei einem Wert gegenübergestellt werden und Match-Ausdrücke müssen
 vollständig sein, d.h. alle möglichen Fälle müssen bei einem Match abgedeckt sein.
 Existiert kein passendes Pattern bei einem Match führt das zu einer Exception.
| triviales Pattern Matching ! | ||
| Beispiel: Funktionsapplikation | # ( fun x -> expr ) ( value );; | 
Wird eine Funktion mit einem Wert des Parametertyps aufgerufen, so wird dieser an x gebunden,
 da jeder Wert des Parametertyps mit den Pattern übereinstimmt.
Die Variable x steht dabei für ein triviales Pattern, da keine Aussage über die Form des Ausdrucks vorgenommen wird,
und damit folglich immer mit einem Wert übereinstimmt.
| triviales Pattern Matching ! | ||
| Beispiel: Mehrfachauswahl | 
 | |
| vereinfacht: | 
 | 
Die Variable c dient als Wildcard, da sie im Rumpf (false) nicht gebunden wird (Beispiel oben).
In der Regel verwendet man dafür die Wildcard "_", welche als "don't care" bezeichnet wird und auf jeden beliebigen Wert passt.
Der Wert kann jedoch dabei nicht mehr abgefragt werden.Dafür kann diese namenslose Variable im Gegensatz zu normalen Variablen,
 welche in einem Pattern nur einmal vorkommen dürfen, mehrfach auftreten und dabei für möglicherweise verschiedene
 Werte stehen.
Die Hauptanwendung von Pattern Matching ist jedoch die Zerlegung von Datenstrukturen, wie Listen, Tupel und Records.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Polymorphismus | 
Polymorphie ("Vielgestaltigkeit") ist ein wichtiges Konzept objektorientierter und funktionaler Sprachen.
Die unterschiedlichen Erscheinungsformen haben als gemeinsamen Nenner, daß allgemein ein Ausdruck für mehrere,
 unterschiedliche Typen definiert ist.
| Polymorphismus | In OO-Sprachen: 
 | |
| parametrischer Polymorphismus | In ML-Sprachen: 
 | |
| Typvariable | Platzhalter für beliebige Typen | |
| Let-bound Polymorphismus | 
 | 
Man unterscheidet im Allgemeinen zwischen parametrischer und Ad-hoc-Polymorphie.
Parametrische Polymorphie liegt vor, wenn Funktionen gleichartig auf einer ganzen Klasse von Datenobjekten wirken.
Typisch für die Ad-hoc-Polymorphie objektorientierter Sprachen sind
überladene Operatoren wie z.B. die arithmetischen Funktionen, die für
Ganzzahlen
und Gleitkommazahlen definiert sind, oder die Gleichheitsfunktion, die
für unterschiedliche
Typen unterschiedlich definiert werden muss, aber dennoch eine feste
Semantik
besitzt. Hier steht dasselbe Symbol für eine Reihe von
unterschiedlichen Funktionen, von
denen je nach Kontext die passende ausgewählt wird.
Das Hindley/Milner-Typsystem unterstützt nur die parametrische Polymorphie, die in der ML-Sprachfamilie durch die
 Let-Bindung und Restriktionen, welche sich aus den nachteiligen Effekten der imperativen Sprachkonstrukte ergeben,
  eingeschränkt wird.
Die Interaktion zwischen Polymorphismus und Seiteneffekten der imperativen Konstrukte in der ML-Sprachfamilie stellt allerdings seit jeher ein ärgerliches Problem dar.
| Beispiel: Polymorphe Funktion | # let identity x = x;; val identity : 'a -> 'a = <fun> | |
| # identity 1;; - : int = 1 | ||
| # identity "Hello";; - : string = "Hello" | 
Die inferierte Typvariable 'a bezeichnet einen sogenannten Typparameter und lässt die Funktion identity offen
für beliebige Datentypen.
Die Funktion identity erwartet also einen Wert beliebigen Typs und liefert genau diesen zurück.
Generell sind Funktionen in OCaml, welche lediglich Werte umkopieren (Komposition und Dekomposition von eigenen Datentypen) oder
relationale Operatoren verwenden polymorph, also für beliebige Typen anwendbar.
| Polymorphismus ist beschränkt auf Werte ! | ||
| Beispiel: Partielle Applikation | # let identity' = identity identity;; val identity : '_a -> '_a = <fun> | |
| # identity' 1;; - : int = 1 | ||
| # identity';; - : int -> int = <fun> | 
Die Typvariable '_a ist kein Typparameter, sondern ein unbekannter Typ, welcher auf eine Wertzuweisung (Instanzierung) wartet.
Danach ist der Typ der Variablen '_a auf den Typen des zugewiesenen Wertes permanent festgelegt.
Eine Lösung für diese Problematik ist die Eta-Expansion, d.h. die explizite Angabe von Argumenten.
| Werte | 
 | |
| Keine Werte | 
 | 
| Lösung durch explizite Angabe von Argumenten ! | ||
| Beispiel: Eta-Expansion | # let identity' x = ( identity identity) x;; val identity' : 'a -> 'a = <fun> | 
Durch die explizite Angabe des Parameters x und entsprechender Klammerung erhält man wieder eine polymorphe Form der Funktion.
| Beispiel: Overloading in Java | 
 | 
Der Plus-Operator (+) ist beispielsweise in Java überladen für Ganzzahlen, Fließkommazahlen, sogar für Zeichenketten, wo er die Konkatenation repräsentiert. Jedoch fordert der Ad-hoc Polymorphismus eine eindeutige Angabe (hier durch die explizite Festlegung der Parametertypen) zur Auswahl der entsprechenden Methode und Kompilierung des/der entsprechenden Maschinenbefehle zur Berechnung. Im Gegensatz zu Java unterstützt OCaml kein Überladen von Funktionen und Methoden.
| Annahme: | Plus-Operator (+) überladen für int und float | |
| # let add x y = x + y;; | ||
| Typ der Funktion ? | val add : int -> int -> int oderval add : float -> float -> float | 
OCaml unterstützt kein Überladen von Methoden und Funktionen, da für den Mechanismus der Typinferenz unentscheidbare Situationen (vor allem auch durch Seiteneffekte) nicht ausgeschlossen werden können.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Produktypen | 
| Produkttyp |  | |
| Record | Benannte Sammlung von Werten beliebigen Typs ! | 
Produkttypen (n-Tupel) wie auch Records dienen dazu mehrere Werte eventuell unterschiedlicher Typen zu einem Ganzen zusammenzufassen.
| Beispiel: Tripel | # let ones = (1, 1.0, "One");; val ones : int * float * string = (1, 1.0, "One") | 
Das Tripel ones ist definiert als das Produkt der Typen int, float und string.
Die Zerlegung (Dekomposition), d.h. der Zugriff auf einzelne Elemente wird realisiert durch triviales Pattern Matching.
| Zugriff auf einzelne Elemente erfolgt durch Pattern Matching | ||
| Beispiel: Dekomposition von Paaren | # let first ( x, _ ) = x;; val first : 'a * 'b -> 'a = <fun> | |
| # let second ( _ ,y ) = y;; val second : 'a * 'b -> 'b = <fun> | ||
| Umgruppierung von Paaren | # let swap ( x, y ) = y, x;; val swap : 'a * 'b -> 'b * 'a = <fun> | |
| Paarbildung | # let pairfirst ( x, _ ) ( y, _ ) = x, y;; val pairfirst : 'a * 'b -> 'b * 'c -> 'a * 'c = <fun> | 
Alle hier gezeigten Funktionen sind polymorph, d.h. eine Verarbeitung von Paaren beliebigen Typs ist möglich.
| Sinnvolle Auswahl und Einsatz von Datentypen ! | ||
| Beispiel: Tupel als Verbund : Schlecht ! | # type stud = string * int * int;; | |
| Record als Verbund : Besser ! | 
 | 
Die Verwendung von Tupeln zur Speicherung von Datensätzen beispielsweise einer Datenbank ist nicht sinnvoll.
Der Einsatz von Records stellt hierfür eine geeignete Lösung dar. Die benannten Feldkomponenten spiegeln hierbei
 deutlicher die problemspezifische Struktur der verbundenen Daten wieder, und die Werte der einzelnen Elemente können
 während des Programmlaufs verändert werden, sofern die Datenfelder veränderlich (mutable) deklariert sind.
Neben der Möglichkeit Records durch Pattern Matching zu zerlegen, kann
über die definierten Label auf die Feldkomponenten zugegriffen werden.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Summentypen | 
Summentypen realisieren unter anderem die Möglichkeit der Verwendung von Aufzählungstypen.
Der vorrangige Einsatzbereich ist jedoch die Definition eigener rekursiver Datenstrukturen. Komplexe, zusammengesetzte,
 parametrisierbare Datenstrukturen (z.B. Bäume) für beliebige Datentypen lassen sich einfach als Summentyp erstellen
 und vor allem verarbeiten.
| Summentyp |  | 
| Beispiel: Aufzählungstyp | 
 | 
Aufzählungstypen sind in funktionalen Sprachen in der Regel Spezialfälle von Summentypen in Form einer Typdefinition mit Hilfe von Konstruktoren, die für jeweils eine Konstante stehen.
| Erstellung und Verarbeitung komplexer Datenstrukturen | ||
| Beispiel: Binär-Baum für beliebige Datentypen | 
 | |
| Erzeugung durch Konstruktoren | ||
| # Node(1, Leaf, Leaf);; - : int btree = Node(1, Leaf, Leaf) | 
Node und Leaf sind hierbei Konstruktoren, also Funktionen die eine (mit Werten initialisierte) Variante des Summentyps
 zurückliefern.
 Der Resultatwert des Konstruktors Node steht dabei für eine benannte Konstante.
| Verarbeitung erfolgt durch Pattern Matching | ||
| Beispiel: Kardinalität von Binär-Baum | 
 | |
| Zugehörigkeit zu Binär-Baum | 
 | 
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Grundlegende Konzepte - Listen | 
Da Listen in der Regel ein elementares und häufig eingesetztes Sprachkonstrukt darstellen, ist in OCaml eigens dafür ein teilweise an Lisp angelehnter Datentyp integriert.
| Liste | Leere oder nicht leere Sequenz von Elementen gleichen Typs | |||
| Konstruktoren | [] | Leere Liste | ||
| e::l | Neue Liste mit | |||
| [e1; ... ; en] | Liste der Elemente e1 bis en | |||
| Listen sind parametrisierte Summentypen ! | ||
| Beispiel: Interne Realisierung des Listentyps | 
 | 
Listen in OCaml sind parametrisierte Summentypen, da eine Liste entweder die leere Liste beschreibt oder eine (konstruierte) Liste beliebigen Typs, bestehend aus einem Element beliebigen Typs (Label) und einer Restliste des selben beliebigen Typs.
| Beispiel: Liste von Funktionen | 
 | 
| Beispiel: Mapping auf Listen | 
 | 
| Anwendung: | # map incr [1; 2; 3; 4];; - : int list = [2; 3; 4; 5] | 
Die Mächtigkeit der Ausdruckskraft funktionaler Programmiersprachen
zeigt sich durch die Einfachheit bei der Manipulation und Berechnung
von Werten.
Hier sei als Beispiel das Mapping von polymorphen Funktionen auf
polymorphen Listen genannt.
Das Mapping bezeichnet hierbei die Anwendung einer beliebigen Funktion auf alle Elemente einer Liste beliebigen Typs.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Gliederung | 
Weiterführende Konzepte
|  | OCaml Konzepte: Weiterführende Konzepte - Das Modulsystem von OCaml | 
| Konzept | 
 | |
| Komponenten | 
OCaml stellt ein mächtiges System zur Strukturierung und einfachen Wiederverwendbarkeit von Programcode zur Verfügung.
Dieses System besteht aus den drei Komponenten Signatur, als Konzept der Bereitstellung von aussagekräftigen Schnittstellen,
Strukturen zur Kapselung von einzelnen, elementaren Programmteilen, und Funktoren, welche Module auf andere Module abbilden
 und das Konzept der Parametrisierung auf Programmteile ausweitet.
Man erkennt hierbei gewisse Ähnlichkeiten bzw. Gemeinsamkeiten zu objektorientierten Designkonzepten, die OCaml ebenfals dem Nutzer bereitstellt.
| Signatur | Deklaration von Typen und Funktionen | |
| Syntax: | 
 | 
| Struktur | Implementation der Deklarationen | |
| Syntax: | 
 | 
| Funktor | 
 | |
| Syntax: | 
 | 
Module sind das grundlegende Strukturierungsmittel für größere Programme und beschreiben seperat
 kompilierbare Einheiten.
Standard ML war die erste Sprache, für die ein theoretisch fundiertes, parametrisches Modulsystem entworfen
wurde.
Die Zuordnung einer Signatur zu einer Struktur beschränkt den Zugriff von außen auf die
 deklarierten Typen und Methoden (Sichtbarkeit). Eine Modulsignatur fungiert somit als Schnittstelle für Module und
  kann als Interface für solche betrachtet werden.
Module können geschachtelt werden und Module können in andere Module eingebunden werden (Erweiterbarkeit).
Es besteht dabei zum einen die Möglichkeit den Namensbereich einer Schnittstelle bzw. eines Moduls zu öffnen
 (eingeleitet durch das Schlüsselwort open), zum anderen kann der Inhalt einer anderen Schnittstelle bzw. eines Moduls
 inkludiert werden (eingeleitet durch das Schlüsselwort include).
Die grundlegende Idee, Module mit Hilfe von abhängigen Typen (dependent types) zu typisieren spiegelt sich im Konzept
 eines Funktors wieder, der Module auf Module abbildet.
  Die Anwendung eines Funktors ist generativ, was bedeutet, dass seine Anwendung jeweils neue Typen erzeugt.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Weiterführende Konzepte - Das Objektsystem von OCaml | 
| Konzept | 
 | |
| Komponenten | 
Objective Caml ist die erste auf ML basierende Sprache, welche objektorientiertes Programmieren erlaubt.
Damit wurde diese Sprache erneut durch ein mächtiges Sytem und weiterführende Konzepte erweitert.
Auffallend dabei ist der analog zum Modulsystem gestaltete Aufbau des Objekt-Layers.
Dieser besteht auch wiederum aus drei elementaren Konstrukten: dem Klassentyp, der Klasse und dem Objekt.
| Klassentyp | 
 | |
| Syntax: | 
 | 
| Klasse | Implementation der Deklarationen | |
| Syntax: | 
 | 
| Objekt | Instanz einer Klasse | |
| Syntax: | 
 | 
Dem aus dem Modulsystem bekannten Konzept der Erweiterbarkeit steht das Objektorientierte Konzept der Vererbung gegenüber.
OCaml unterstützt Mehrfachvererbung von Klassen.
Dabei wird die jeweils letzte Definition einer Methode beibehalten.
 Die erneute Definition einer Methode in einer Unterklasse überschreibt die Definition der Oberklasse.
Im Gegensatz zu anderen Objektorientierten Sprachen (C++, Eiffel) unterscheidet OCaml zwischen Subtyphierarchie und
 Vererbungshierarchie, d.h. Unterklassen sind von einem anderen Typ als die jeweilige Oberklasse.
Der Mechanismus der späten Bindung "Late-Binding", also dem dynamischen Binden ergänzt
 den parametrischen Polymorphismus. Die Sichtbarkeit von Methoden und Typen wird durch Schnittstellendefinitionen beschränkt.
 Generell ist ein Zugriff auf Methoden der Klassen von außen möglich, ein Zugriff auf Attribute hingegen nicht.
Hierfür sind unweigerlich Setter und Getter-Methoden erforderlich. Ein Zugriff auf Methoden einer jeweiligen Oberklasse
ist durch das Schlüsselwort super möglich, sowie eine Selbstreferenz auf Objekte durch Vereinbarung einer Variablen,
 die an das Objekt gebunden wird.
 Abstrakte Methoden und Klassen werden deklariert durch das einleitende Schlüsselwort virtual.
  Eine Abstrakte Klasse ist dabei definiert als eine Klasse, die mindestens eine abstrakte Methode beinhaltet.
 Durch die Instanzierung einer Klasse eingeleitet durch das Schlüsselwort new, welches zur Erstellung eines Objektes dient,
  wird der Klassenrumpf im gleichen Moment vollständig ausgewertet. Das ist wohl der gravierendste Unterschied zu
   bekannten objektorientierten Sprachen wie beispielsweise Java.
Hinweis:
"#" kennzeichnet Eingaben im OCaml Interpreter,
"val" bzw "- :" das Ergebnis der Auswertung und automatischen Typanalyse inkl. der vom System inferierten Typen.
|  | OCaml Konzepte: Weiterführende Konzepte - 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.
| Beispiel: Modul | 
 | |
| 
 | ||
| 
 | 
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.
| Beispiel: Funktor | 
 | 
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.
Zum Vergleich mit den folgenden objektorientierten Implementierungen in Ocaml ein entsprechender ADT in Java.
| Beispiel: Klassen in Java | 
 | |
| 
 | 
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).
| Beispiel: Generische Klassen | 
 | |
| 
 | ||
| 
 | 
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 | 
 | |
| 
 | ||
| 
 | 
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.
|  | OCaml Konzepte: Quellen und weiterführende Literatur | 
Quellen und weiterführende Literatur
The O'Caml Language: "The programming tool of choice for discriminating hackers"
http://www.ocaml.org/
[Hickey, Jason][Hoisie, Michael B.]
- Introduction to the Objective Caml Programming Language [pdf]
http://www.cs.caltech.edu/courses/cs134/cs134b/book.pdf[Matuszek, David]
- Ocaml Programming - A Practical User's Guide
http://www.ocf.berkeley.edu/~mbh/tutorial/[O'Reilly Verlag]
- A Concise Introduction to Objective Caml
http://www.csc.vill.edu/~dmatusze/resources/ocaml/ocaml.html[Rémy , Didier]
- Developing Applications with Objective Caml [html]/[pdf]
http://caml.inria.fr/oreilly-book/
- Using, Understanding, and Unraveling The OCaml Language :
"From Practice to Theory and vice versa"
http://pauillac.inria.fr/~remy/cours/appsem/