3.2.1 Definition und Verwendung von Typen


Unter einem Datentyp (im folgenden nur Typ) versteht man eine Menge von zulässigen Werten als Operanden und Ergebnisse sowie von darauf anwendbaren Operationen in einer Programmiersprache [InformatikLexikon, S. 215].

Typ-Definitionen in F# sind dank der aus ML geborgten Notation aussagekräftiger als in C#. Insbesondere algebraische Datentypen sind bei der Programmierung hilfreich und wären in C# und anderen objektorientierten Sprachen aufwendig zu formulieren.

In F# gibt es folgende Arten von TypenEnum-, Delegat-, Exception- und Maßeinheit-Typen werden hier nicht aufgeführt, weil es sich um besondere Structs oder Klassen bzw. Maßeinheiten-Metadaten handelt. Genaueres über diese Typen findet sich in der F#-Spezifikation [FSharpSpec, vgl. S. 115-116].:

Funktionstyp

In einer funktionalen Programmiersprache ist das wichtigste Element die Funktion. Eine Funktion bildet Eingabe-Werte auf Ausgabe-Werte ab.

Funktionen sind Werte von Funktionstypen. Funktionstypen haben die Form Eingabetyp -> Ausgabetyp, wobei Eingabe- und Ausgabetyp beliebige Typen sind. Der Typ der Quadrierungsfunktion (x \mapsto x^2) ist leicht verständlich: int -> int. Im Fall von partieller Applikation sehen Funktionstypen etwas interessanter aus. Arithmetische Operationen (etwa +, -, *, /) und Vergleiche (wie >, <=) erwarten zwei Argumente, um ausgeführt zu werden. Die Applikation des Operators auf das erste Argument liefert eine Funktion, die nur noch das zweite Argument erwartet. Das wird im Fall der Addition für Ganzzahlen so dargestellt: int -> (int -> int). Der Eingabetyp ist int, d.h. der erste Eingabewert ist eine ganze Zahl und der Ausgabetyp ist eine Funktion vom Typ int -> int. Dies stellt den Typen der Funktion dar, die das zweite Argument erwartet. Die Klammerung von Funktionstypen ist rechtsassoziativ, deshalb wird statt int -> (int -> int) gerne int -> int -> int geschrieben.

Ein Sonderfall von Funktionstypen sind die Typen von Funktionen höherer Ordnung. Map funktioniert auf Listen eines beliebigen aber festen Typs (nennen wir den Elementtyp 'a). Der Funktionstyp von Map lautet ('a -> 'b) -> 'a list -> 'b list. Es wird als erstes Argument eine Funktion erwartet, die einen Wert vom Typ 'a auf einen Wert vom Typ 'b abbildet (man sagt kürzer ein 'a auf ein 'b abbildet. Die Klammerung um Funktionstypen bei Parametern ist wichtig.) Als zweites Argument wird eine Liste von 'as erwartet; wird die Funktion darauf angewendet, wird eine Liste von 'bs geliefert, deren Elemente aus den Elementen der Eingabeliste und der Abbildungsfunktion entstehen.

Die Definition von Funktionstypen geschieht in F# implizit durch das Definieren von Funktionen.

Algebraischer Datentyp


Algebraische Datentypen (kurz ADT, auch union types) sind Typen, die eine abgeschlossene Anzahl an Varianten haben. Sind den Varianten keine zusätzlichen Daten zugeordnet, sind dies Werte des Typs; andernfalls nennt man die instanziierten Varianten Werte des Typs. Varianten sind oft Daten von Produkttypen zugeordnet. Produkttypen sind die Typen von n-Tupeln (Typen die durch Bildung des kartesischen Produktes über n Typen entstehen).

Bearbeitet: Minimale algebraische Definition und Varianten vs. Werte

Beispiele sind Bool’sche Werte (Typ bool mit den zwei Varianten bzw. Werten true und false) oder das Nulltupel (Typ unit mit dem Wert ()), aber auch Datenbehälter wie etwa der Typ aus einer Variante Punkt (mit allen Werten der Form Punkt(a, b) mit konkreten Ganzzahlen a und b).

Wenn man von mehr als einer Variante spricht, nennt man diese auch Summentypen und verwendet sie um inhaltlich verwandte Elemente, die aber strukturell unterschiedlich aufgebaut sein können, zusammenzufassen [FunktProg, S. 17].

Wichtig sind vor allem generische algebraische Datentypen (kurz GADT), die beliebige aber festen Elementtypen enthalten können, sodass strukturierte Daten konstruiert und verarbeitet werden können. Das klassische Beispiel ist eine Liste eines bestimmten Elementtyps. Eine Liste von ‚a (Liste von Elementtyp 'a) lässt sich so definieren:

  • Die leere Liste ist eine Liste von ‚a.
  • Eine Element vom Typ 'a gefolgt von einer Liste von 'a ist eine Liste von 'a.
  • Nur diese beiden Varianten erzeugen Listen von 'a.

Den beiden Varianten müssen Namen gegeben werden, um mit ihnen zu arbeiten: Die leere Liste wird Nil und das Paar von Element und Liste wird Cons genannt. [1,2,3,4] ist eine Liste von Ganzzahlen (int list) und ist aus den beiden Varianten konstruiert: Cons(1, Cons(2, Cons(3, Cons(4, Nil)))). Eine wichtige Infix-Schreibweise ist 1::2::3::4::[], wobei [] für Nil und a :: b für Cons(a, b) steht und rechtsassoziativ ist (a :: b :: c = a :: (b :: c)). Die Liste stellt eine der wichtigsten Datenstrukturen in der funktionalen Programmierung dar. Sie ist ein Beispiel für rekursiv definierte Typen; ein anderes Beispiel ist der Typ von Binärbäumen. Der Binärbaum Node(2, Node(1, Node(0, Empty, Empty), Empty), Node(4, Node(3, Empty, Empty), Node(5, Empty, Empty))) lässt sich wie in Abbildung # visualisieren.


baum
Ein Binärbaum

Andere wichtige generische algebraische Datentypen sind die Typen von Tupeln ('a * 'b), Tripeln ('a * 'b * 'c), Quadrupeln ('a * 'b * 'c * 'd) usw., wobei die Typen der Komponenten beliebig aber fest sind. Algebraische Datentypen mit einer Variante sind nützlich, aber eher ungebräuchlich, da auf Tupel oder Datensätze für diesen Zweck zurückgegriffen werden kann. Außerdem wird im Falle von partiell definierten Funktionen Gebrauch vom generischen Option-Typ gemacht, der aus den Varianten None oder Some(x) mit Werten x vom Typ 'a besteht. Z.B. kann die ganzzahlige Division so dargestellt werden, dass für Werte a und b das Ergebnis None im Fall von b = 0 und Some(a/b) sonst ist.

Algebraische Datentypen werden in F# folgendermaßen definiert:

type Unit = Unit // eingebaut als Typ unit
type Bool = True | False // eingebaut als primitiver Typ bool
type Punkt = Punkt of int * int
type List<'a> = Nil | Cons of 'a * List<'a>
// eingebaut als Typ list<'a>
type Binary<'a> = Empty | Node of 'a * Binary<'a> * Binary<'a>
type Option<'a> = None | Some of 'a // eingebaut als Typ option<'a>
type Tupel<'a, 'b> = Tupel of 'a * 'b // eingebaut als Typ 'a * 'b
type Tripel<'a, 'b, 'c> = Tripel of 'a * 'b * 'c
// eingebaut als Typ 'a * 'b * 'c

Wie eingangs erwähnt und wie aus den Beispielen deutlich wird, ist die Notation äußerst kompakt. Vergleichbare Definitionen in objektorientierten Sprachen sind wesentlich länger, weil dem Programmierer auferlegt ist, Klassen, Unterklassen, Konstruktoren und Felder zu schreiben. Außerdem sind korrekte Equals– und GetHashCode-Methoden zu implementieren. Gerade bei algebraischen Datentypen muss sichergestellt werden, dass keine weiteren Varianten durch Unterklassenbildung hinzugefügt werden können.

Der deklarative Stil der obigen Defintionen ist bemerkenswert. Nicht nur bei der Definition, sondern auch bei der Verwendung haben algebraische Datentypen Vorteile gegenüber Klassen, siehe dazu den Eintrag Algebraischer Datentyp im Abschnitt Pattern Matching.

Datensatz


Datensätze (record types) sind Typen, die als Datenbehälter fungieren. Die Verwendung von Datensätzen ist Tupeln vorzuziehen, wenn komplexere Datenstrukturen als Argument übergeben oder von Funktionen geliefert werden. Tupeln gegenüber haben die Bestandteile von Datensätzen keine feste Ordnung, jedoch einen Namen.

Eine Person sollte in einem Programm nicht als Tupel vom Typ (string * string) dargestellt werden, wobei die Komponenten Vorname und Nachname darstellen sollen. Stattdessen kann ein Datensatz definiert werden:

type Person = { Vorname: string; Nachname: string }
// Beispiel für ein Datensatzobjekt:
let MsCarter = { Vorname = "Samantha"; Nachname = "Carter" }

Die Definition solcher Datenbehälter ist auf das Nötigste reduziert; insbesondere muss man keine Konstruktoren oder Equals-Methoden implementieren, was fehlerträchtig sein kann und in vielen objektorientierten Sprachen für jeden Typen von Hand getan werden muss; eine Betrachtung dazu findet sich im Abschnitt Imperativ-objektorientierte Typen. Zwei zusätzliche Eigenschaften sind bei Datensätzen zu erwähnen:

  1. Muster zur Extraktion von Daten stehen zur Verfügung, siehe dazu den Eintrag Datensatz im Abschnitt Pattern Matching.
  2. Geringfügige Änderungen von Datensätzen können mit dem with-Operator durchgeführt werden, wobei eine neuer Datensatz erstellt wird, welche dem Vorlage-Datensatz-Objekt bis auf die angegeben Felder gleicht:

    let MrsMcKay = { MsCarter with Nachname = "Mc Kay" }
    

Klasse und Interface


Eine Klasse ist der Typ eines Objektes. Objekte haben einen internen Zustand, der nur durch Operationen verändert oder sondiert werden darf. Klassen können voneinander abgeleitet sein, wodurch die Unterklasse Eigenschaften (Zustand und Verhalten) von der Oberklasse übernimmt. Der Zustand kann in Unterklassen erweitert und Verhalten abgeändert werden.
Die Menge der Operationen einer Klasse ist ihre Schnittstelle.


Interfaces sind lediglich eine Menge von Operationen ohne Implementierung. Klassen können Interfaces implementieren, wodurch Objekte dieser Klassen unter der Sicht des entsprechenden Interfaces benutzt werden können.

Bearbeitet: Operation->Methode = Dynamisches Binden; Austauschen = Subtyp-Polymorphie

Das Aufrufen von Operationen an Objekten und das Auswählen der klassenspezifischen Methode, um die Operation auszuführen, nennt sich dynamisches Binden. Die Idee ist, dass der Aufruf einer Operation nicht fest an eine Methode gebunden ist, sondern flexibel zur Laufzeit ausgetauscht werden kann. Das Austauschen geschieht über Subtyp-Polymorphie (Subtypen sind die oben erwähnten Unterklassen; Polymorphie steht für Vielgestaltigkeit), d.h. Klassenvererbung und Überschreiben von Methoden. Diese Flexibilität ist Grundlage vieler Entwurfsmuster [Entwurfsmuster, vgl. S. 16] und wird als Technik im Abschnitt Imperativ-objektorientierte Fallunterscheidungen erläutert.

Struct


Structs stellen Typen von Werten dar, die nicht wie Objekte auf dem Heap sondern auf dem Aufrufstack gespeichert werden. Dieser Aspekt hat und Vor- und Nachteile, die erst betrachtet werden müssen, wenn Performanz eine Rolle spielt. Diese Betrachtung wird im Abschnitt Imperativ objektorientierte Typen vorgenommen.

Hinterlasse einen Kommentar