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
http://www.fh-wedel.de/~si/index.html
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
http://www.fh-wedel.de/~si/seminare/ws04/Ausarbeitung/b.OCaml/OCaml0.htm
aufrufbar.
Wir danken Hr. Prof. Uwe Schmidt für die Einverständniserklärung.

JoeCaml OCaml Konzepte:

Kombination von
funktionalen und objektorientierten Konzepten
am Beispiel OCaml


Informatikseminar WS 2004/2005
Sven Nowak, mi5226

Gliederung


[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Einleitung ] [ Grundlegende Konzepte ] [ Weiterführende Konzepte ] [ Quellen und weiterführende Literatur ]
JoeCaml OCaml Konzepte: Einleitung

Diagramm

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.


OCaml unterstützt alle Konzepte moderner Programmiersprachen.

Dazu gehören:


[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ]
JoeCaml OCaml Konzepte: Gliederung

Grundlegende Konzepte


[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Weiterführende Konzepte ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Das Typensystem von OCaml

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
Die Typen der Unterausdrücke werden zu einem Gesamtausdruck synthetisiert


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.


[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Die Auswertungsstrategie von OCaml ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Die Auswertungsstrategie von OCaml

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
  • Konstanten Funktionen/Operationen werten sich zu sich selbst aus
  • Funktionsrümpfe werden erst bei Funktionsapplikation ausgewertet
  • Bei Funktionsapplikation wird jeder Parameter innerhalb des Funktionsrumpfes durch ausgewertetes Argument ersetzt
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

Funktion 1: zero x = 0

Funktion 2: square x = x * x

Applikation:

zero(square(square(square(2))))
*
*
*
*
zero(256) 0

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

Funktion 1:

cond c e f = if c then e else f

Funktion 2:

sum n =
if (n = 0)
then n
else (n + sum (n - 1))

Funktion 3:

sum n =
cond (n = 0) n (n + sum (n - 1))

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.


[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Variablen in OCaml ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Variablen

Variablen in OCaml

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

# let x = 1 in
let x = 2 in
let y = x + x in x + y;;

- : int = 6

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.



[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Funktionen in OCaml ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Funktionen

Funktionen in OCaml

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:
  • Werte wie alle anderen Werte
  • Standardmäßig anonym
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 !


# let derive f dx x = ((f (x +. dx) -. f x ) /. dx);;

val derive : (float -> float) -> float -> float -> float = <fun>

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.



[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Pattern Matching ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Pattern Matching

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:
  • Variablen
  • Konstanten
  • Wildcards
Pattern Matching Mechanismus:
  • Gegenüberstellung und Vergleich von Pattern und Wert
  • Variablenbindung bei Übereinstimmung
Syntax:

# match expr with
patt1 -> expr1
| patt2
| patt3 -> expr2
| pattn -> exprn;;

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

# let is_digit d =
match d with
'0'
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9' -> true
| c -> false;;

vereinfacht:

# let is_digit d = function
'0' .. '9' -> true
| _ -> false;;

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.



[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Polymorphismus in OCaml ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Polymorphismus

Polymorphismus in OCaml

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:
  • Ad hoc - Polymorphismus: Überladen von Methoden/Funktionen
  • Subtyppolymorphismus: Objekte, die den selben Vorfahren besitzen, teilen oder redefinieren gleiche Operationen
parametrischer Polymorphismus In ML-Sprachen:
  • Typparametrisierung implizit bei der Hindley-Milner Typüberprüfung
  • Typen können explizit mit Typvariablen parametrisiert werden
  • Polymorphe, generische Funktionen, Klassen, Module, ...
Typvariable

Platzhalter für beliebige Typen

Let-bound Polymorphismus
  • Restriktion im Hindley-Milner Typsystem
  • Polymorphismus ist beschränkt auf Werte

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

  • Funktionen
  • Konstanten
  • Konstruktoren unveränderlicher Werte
Keine Werte

  • Funktionapplikationen
  • Kontrollstrukturen: z.B. Pattern Matching
  • Konstruktoren von Typen mit veränderlichen Werten

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

class Adder {
static int Add (int i , int j){ return i + j;
}
static float Add (float i , float j){ return i + j;
}
}

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
oder
val 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.



[ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Produkttypen in OCaml ]
JoeCaml OCaml Konzepte: Grundlegende Konzepte - Produktypen

Produkttypen in OCaml

Produkttyp

  • Generalisierung des Kartesischen Produkts
  • Tupel beliebiger evtl. unterschiedlicher Typen
  • 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 !

    # type student =
    {name : string;
    regNo : int;
    mutable semester : int;
    };;

    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.



    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Summentypen in OCaml ]
    JoeCaml OCaml Konzepte: Grundlegende Konzepte - Summentypen

    Summentypen in OCaml

    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

  • Menge optionaler Typen
  • Bekannt als: (Disjoint) Unions bzw. Variante Records

  • Beispiel:
    Aufzählungstyp

    # type color = Blue
    | Green
    | Red
    | Yellow

    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

    # type 'a btree = Leaf
    | Node of 'a btree * 'a * 'a btree;;


    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

    # let rec card = function Leaf -> 0
    | Node( _ , left, right ) -> card left + card right + 1;;

    val card : 'a btree -> int = <fun>

    Zugehörigkeit zu Binär-Baum

    # let rec member x = function
    Leaf -> false
    | Node( y , left, right ) ->(x = y)
    || (member x left)
    || (member x right);;

    val member : 'a -> 'a btree -> bool = <fun>



    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 ] [ Grundlegende Konzepte ] [ Listen in OCaml ]
    JoeCaml OCaml Konzepte: Grundlegende Konzepte - Listen

    Listen in OCaml

    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
  • e = head (Erstes Element)
  • l = tail (Restliste)
  • [e1; ... ; en] Liste der Elemente e1 bis en


    Listen sind parametrisierte Summentypen !

    Beispiel:
    Interne Realisierung des Listentyps

    # type 'a list = [] | :: of 'a * 'a list;;

    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

    # let addlist =
    let add x y = x + y in [add 1; add 2; add 3];;

    val addlist : (int -> int) list = [<fun>; <fun>; <fun>]


    Beispiel:
    Mapping auf Listen

    # let rec map f = function [] -> []
    | x::l -> (f x)::(map f l);;

    val map : ('a -> 'b) -> 'a list -> 'b list = <fun>

    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.



    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Grundlegende Konzepte ] [ Weiterführende Konzepte ]
    JoeCaml OCaml Konzepte: Gliederung

    Weiterführende Konzepte


    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Quellen und weiterführende Literatur ]
    JoeCaml OCaml Konzepte: Weiterführende Konzepte - Das Modulsystem von OCaml

    [weiter]

    Das Modulsystem von OCaml

    Konzept
    • Kapselung von Informationen und Operationen
    • Information Hiding
    • Wiederverwendbarkeit
    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.


    [weiter]

    Signatur Deklaration von Typen und Funktionen

    Syntax:

    # module type SigName =
    sig
    signature
    end;;


    [weiter]

    Struktur Implementation der Deklarationen

    Syntax:

    # module Name =
    struct
    implementation
    end;;


    [weiter]

    Funktor
    • Funktion auf Modulen
    • parametrisierte Struktur
    Syntax:

    # module FName (ArgName : ArgSig) =
    struct
    implementation
    end;;


    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.



    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Weiterführende Konzepte ] [ Das Objektsystem von OCaml ]
    JoeCaml OCaml Konzepte: Weiterführende Konzepte - Das Objektsystem von OCaml

    [weiter]

    Das Objektsystem von OCaml

    Konzept
    • Kapselung von Informationen und Operationen
    • Information Hiding
    • Wiederverwendbarkeit
    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.


    [weiter]

    Klassentyp
    • Signatur für Klassen
    • Deklarationen
    Syntax:

    # class type name =
    object
    declarations
    end;;


    [weiter]

    Klasse Implementation der Deklarationen

    Syntax:

    # class Name =
    object
    implementation
    end;;


    [weiter]

    Objekt Instanz einer Klasse

    Syntax:

    # let obj =new oname;;


    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.



    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ] [ Weiterführende Konzepte ] [ OO- Modulprogrammierung am Bsp. ADT ]
    JoeCaml OCaml Konzepte: Weiterführende Konzepte - OO- Modulprogrammierung am Beispiel ADT

    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

    # 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.


    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.


    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).


    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 ]
    JoeCaml 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.] [Matuszek, David] [O'Reilly Verlag] [Rémy , Didier]


    [ Seminar "Programmierkonzepte und Programmiersprachen" ] [ Gliederung ]