• Keine Ergebnisse gefunden

3. Funktionales Programmieren

N/A
N/A
Protected

Academic year: 2022

Aktie "3. Funktionales Programmieren"

Copied!
20
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 78

• Grundkonzepte funktionaler Programmierung

• Algorithmen auf Listen und Bäumen

• Abstraktion mittels Polymorphie und funktionen höherer Ordnung

• Semantik, Testen und Verifikation

3. Funktionales Programmieren

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 79

0. Vorbemerkungen 1. Einführung

2. Grundkonzepte von Softwaresystemen

3. Funktionales Modellieren und Programmieren 3.1 Grundkonzepte funktionaler Programmierung

3.1.1 Zentrale Begriffe und Einführung 3.1.2 Rekursive Funktionen

3.1.3 Listen und Tupel

3.1.4 Benutzerdefinierte Datentypen 3.2 Algorithmen auf Listen und Bäumen 3.3 Abstraktion mittels Polymorphie und

Funktionen höherer Ordnung 3.4 Semantik, Testen und Verifikation 4. Prozedurales Programmieren

5. Objektorientiertes Programmieren

Inhalt von Kapitel 3:

3.1 Grundkonzepte funktionaler Programmierung

Vorgehen:

• Zentrale Begriffe und Einführung:

Funktion, Wert, Typ, Datenstruktur, Auswertung, ...

• Rekursive Funktionen

• Listen und Tupel

• Benutzerdefinierte Datentypen

3.1.1 Zentrale Begriffe und Einführung

Funktionale Programmierung im Überblick:

• Funktionales Programm:

- partielle Funktionen von Eingabe- auf Ausgabedaten - besteht aus Deklarationen von (Daten-)Typen,

Funktionen und (Daten-)Strukturen

- Rekursion ist eines der zentralen Sprachkonzepte - in Reinform: kein Zustandskonzept, keine verän-

derlichen Variablen, keine Schleifen, keine Zeiger

• Ausführung eines funktionalen Programms:

Anwendung der Funktion auf Eingabedaten

Definition: (Funktionsanwendung, -auswertung, Terminierung, Nichtterminierung)

Bezeichne f eine Funktion und a ein zulässiges Argument von f.

Die Anwendung von f auf a nennen wir eine Funktionsanwendung (engl. function application);

üblicherweise schreibt man dafür f(a) oder f a . Den Prozess der Berechnung des Funktionswerts nennen wir Auswertung (engl. evaluation). Die Auswertung kann:

- nach endlich vielen Schritten terminieren und ein Ergebnis liefern (normale Terminierung, engl.

normal termination),

- nach endlich vielen Schritten terminieren und einen Fehler melden (abrupte Terminierung, engl. abrupt termination),

- nicht terminieren, d.h. der Prozess der Auswertung kommt (von alleine) nicht zu Ende.

Bemerkungen:

Entsprechendes gilt in anderen Programmier- paradigmen.

(2)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 82

Definition: (partielle Funktion)

Ein Funktion heißt partiell, wenn sie nur auf einer Untermenge ihres Argumentbereichs definiert ist.

Andernfalls heißt sie total.

Bemerkungen:

• Da Terminierung nicht entscheidbar ist, benutzt man in der Informatik häufig partielle Funktionen.

• Durch Einführen eines Elements „undefiniert“ kann man jede partielle Funktion total machen.

Üblicherweise bezeichnet man das Element für

„undefiniert“ mit⊥ (engl. „bottom“).

Beispiel: (Zur Entscheidbarkeit der Terminierung) McCarthy‘s Funktion:

Bezeiche N die Menge der positiven ganzen Zahlen und sei m : N N wie folgt definiert:

n -10 , für n >100 m( n ) =

m( m( n+11) ) , für n≤100 Ist m für alle Argumente wohldefiniert?

+

+ +

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 83

Begriffsklärung: (Wert, Value)

Werte (engl. v

alues)

in der (reinen) funktionalen Programmierung sind

- Elementare Daten (Zahlen, Wahrheitswerte, Zeichen, ...),

- zusammengesetzte Daten (Listen von Werten, Wertepaare,...),

- (partielle) Funktionen mit Werten als Argumenten und Ergebnissen.

• In anderen Sprachparadigmen gibt es auch Werte, allerdings werden Funktionen nicht immer als Werte betrachtet.

• Im Mittelpunkt der funktionalen Programmierung steht die Definition von Wertemengen (Datentypen) und Funktionen. Funktionale Programmsprachen stellen dafür Sprachmittel zur Verfügung.

Bemerkungen:

• Wie für abstrakte Objekte oder Begriffe typisch, besitzen Werte

- keinen Ort,

- keine Lebensdauer,

- keinen veränderbaren Zustand, - kein Verhalten.

Begriffsklärung: (Typ, type)

Typisierte Sprachen besitzen ein Typsystem.

Ein Typ (engl. type) fasst Werte zusammen, auf denen die gleichen Funktionsanwendungen zulässig sind. In typisierten Sprachen besitzt jeder Wert einen Typ.

In funktionalen Programmiersprachen gibt es drei Arten von Werten bzw. Typen, mit denen man rechnen kann:

• Basisdatentypen (int,bool,string, ... )

• benutzerdef., insbesondere rekursive Datentypen

• Funktionstypen ( z.B. int bool oder ( int int ) ( int int ) )

Datenstrukturen

Eine Struktur fasst Typen und Werte zusammen, insbesondere also auch Funktionen.

Datenstrukturen sind Strukturen, die mindestens einen „neuen“ Datentyp und alle seine wesentlichen Funktionen bereitstellen.

Eine Datenstruktur besteht aus einer oder mehrerer disjunkter Wertemengen zusammen mit den darauf definierten Funktionen.

In der Mathematik nennt man so ein Gebilde eine Algebra oder einfach nur Struktur.

In der Informatik spricht man auch von einer Rechenstruktur.

(3)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 86

Definition: (Datenstruktur mit Signatur)

Eine Signatur einer Datenstruktur (T,F) besteht aus - einer endlichen Menge T von Typbezeichnern und - einer endlichen Menge F von Funktionsbezeichnern, wobei für jedes f∈F ein Funktionstyp

f : T1 * ... * Tn T0 , Ti∈T, 0≤i≤n, 0≤n, definiert ist. n gibt die Stelligkeit von f an.

Eine (partielle) Datenstruktur mit Signatur (T,F) ordnet

- jedem Typbezeichner T∈T eine Wertemenge, - jedem Funktionsbezeichner f∈F eine partielle

Funktion zu,

so dass Argument- und Wertebereich von f den Wertemengen entsprechen, die zu f‘s Funktionstyp gehören.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 87

• Wir betrachten zunächst die Basisdatenstrukturen, wie man sie in jeder Programmier-, Spezifikations- und Modellierungssprache findet.

• Die Basisdatenstrukturen (engl. basic / primitive data structures) bilden die Grundlage zur Definition weiterer Typen, Funktionen und Datenstrukturen.

• Als Beispiel dienen uns die Basisdatenstrukturen der funktionalen Sprache ML. Später lernen wir auch die Basisdatenstrukturen von Java kennen.

• Wir benutzen Funktionsbezeichner auch mit Infix-Schreibweise.

• Funktionen ohne Argumente nennen wir Konstanten.

• Allgemeinere Formen von Strukturen betrachten wir erst am Ende des Kapitels.

Bemerkungen:

Die Datenstruktur Boolean:

Typ: bool

Funktionen:

= : bool * bool bool

<> : bool * bool bool not: bool bool

Konstanten:

true: bool false: bool

Dem Typbezeichnerbool ist die Wertemenge { true, false } zugeordnet.

bezeichnet die Gleichheit auf Wahrheitswerten

bezeichnet die Ungleichheit auf Wahrheitsw.

bezeichnet die logische Negation.

bezeichnet den Wert true.

bezeichnet den Wert false.

Die Datenstruktur Int:

Die Datenstruktur Int erweitert die Datenstruktur Boolean, d.h. sie umfasst den Typ bool und die darauf definierten Funktionen. Zusätzlich enthält sie:

Typ: int

Funktionen:

= : int * int bool (* Gleichheit *)

<>: int * int bool (* Ungleichheit *)

~ : int int (* Negation *) + : int * int int (* Addition *) - : int * int int (* Subtraktion *)

* : int * int int (* Multiplikation *) abs: int int (* Absolutbetrag *)

Bemerkung:

Im Folgenden unterscheiden wir nur noch dann zwischen Funktionsbezeichner und bezeichneter Funktion, wenn dies aus Gründen der Klarheit nötig ist.

(4)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 90

div: int * int int (* ganzzahlige Division *) mod: int * int int (* Divisionsrest *)

< : int * int bool (* kleiner *)

> : int * int bool (* größer *)

<=: int * int bool (* kleiner gleich *)

>=: int * int bool (* größer gleich *) Konstanten:

- in Dezimaldarstellung: 0, 128, ~245

- in Hexadezimaldarstellung: 0x0, 0x80, ~0xF5

Dem Typbezeichner ist in ML eine rechner- abhängige Wertemenge zugeordnet, typischerweise die Menge der ganzen Zahlen von -2^30 bis 2^30-1.

Innerhalb der Wertemenge sind die Funktionen der Datenstruktur Int verlaufsgleich mit den üblichen Funktionen auf den ganzen Zahlen.

Ausserhalb der Wertemenge sind sie nicht definiert;

insbesondere bezeichnen partielle Funktionen.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 91

Begriffsklärung:

Funktionsbezeichner können überladen werden, d.h. in Abhängigkeit vom Typ ihrer Argumente bezeichnen sie unterschiedliche Funktionen.

Die Datenstruktur Real:

Die Datenstruktur Real erweitert die Datenstruktur Int, d.h. sie umfasst deren Typen und Funktionen sowie:

Typ: real

Funktionen:

~ : real real (* Negation *) + : real * real real (* Addition *) - : real * real real (* Subtraktion *)

* : real * real real (* Multiplikation *) / : real * real real (* Division *) abs: real real (* Absolutbetrag *) Wenn unterschiedliche Funktionen oder andere Programmelemente den gleichen Bezeichner

haben, spricht man vom Überladen des Bezeichners (engl. Overloading).

Beispiel: (Überladung von Bezeichnern)

< : real * real bool (* kleiner *)

> : real * real bool (* größer *)

<=: real * real bool (* kleiner gleich *)

>=: real * real bool (* größer gleich *) real : int real (* nächste reelle Zahl *) round: real int (* nächste ganze Zahl *) floor: real int (* größte ganze Zahl <= *) ceil : real int (* kleinste ganze Zahl >= *) trunc: real int (* ganzzahliger Anteil *) Konstanten:

- mit Dezimalpunkt: , ! , !"# $, %" #$&

- mit Exponenten: ', !(), !"#$(%!,%"#$&'%) Dem Typbezeichnerreal ist in ML eine rechner- abhängige Wertemenge zugeordnet. Entsprechendes gilt für die präzise Semantik der Funktionsbezeichner.

Bemerkung:

• Die ganzen Zahlen sind in der Programmierung keine Teilmenge der reellen Zahlen!

• Keine Gleichheitsoperationen auf reellen Zahlen.

Die Datenstruktur Char:

Die Datenstruktur Char erweitert die Datenstruktur Int, also auch Bool. Zusätzlich enthält sie:

Typ: char

Funktionen:

= : char * char * bool

<>: char * char * bool

< : char * char * bool (* kleiner *)

> : char * char * bool (* größer *)

<=: char * char * bool

>=: char * char * bool

chr: int * char (* char zu ASCII Code *) ord: char * int (* ASCII Code zu char *) Konstanten:

Konstantenbezeichner haben die Form +,α,-wobeiα - ein druckbares Zeichen ist,

- die Form ./, .0, .., .“, ... hat,

- die Form \zzz hat, wobei z eine Ziffer ist und die dargestellte Dezimalzahl ein legaler Zeichencode ist.

(5)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 94

Die Datenstruktur String:

Die Datenstruktur String erweitert die Datenstruktur Char, d.h. sie umfasst deren Typen und Funktionen.

Zusätzlich enthält sie:

Typ: string Funktionen:

= : string * string 1 bool

<>: string * string 1 bool

< : string * string 1 bool (* kleiner *)

> : string * string 1 bool (* größer *)

<=: string * string 1 bool

>=: string * string 1 bool

^ : string * string 1 string

(* Zeichenreihenkonkatenation *) size: string 1 int (* Länge *) substring: string * int * int 1 string

(* Teilzeichenreihe *) str: char 1 string 23 entsprechende

Zeichenreihe der Länge 1 *)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 95

Konstanten:

Zeichenreihen eingeschlossen in doppelte Hochkommas:

“Ich bin ein String!!“ “Ich \098\105\110 ein String!!“

“Mein Lehrer sagt: \“Nehme die Dinge genau!\““

“String vor Zeilenumbruch\n Nachzeilenumbruch“

Dem Typbezeichnerstring ist die Menge der

Zeichenreihen über der Menge der Zeichen zugeordnet, die von der vorliegenden ML-Implementierung

unterstützt werden.

Den Vergleichsoperationen liegt die lexikographische Ordnung zugrunde, wobei die Ordnung auf den Zeichen auf derem Code basiert (siehe Datenstruktur Char).

• Jede Programmier-, Modellierungs- und

Spezifikationssprache besitzt Basisdatenstrukturen.

Die Details variieren aber teilweise deutlich.

• Wenn Basisdatenstrukturen implementierungs- oder rechnerabhängig sind, entstehen

Portabilitätsprobleme.

• Der Trend bei den Basisdatenstrukturen geht zur Standardisierung.

Bemerkung:

• Es wird unterschieden zwischen Zeichen und Zeichenreihen der Länge 1.

• Aufbauend auf der Datenstrukturstringund der Listendatenstruktur stellt ML auch die Funktionen

explode : string 4 char list implode : char list 4 string zur Verfügung.

Aufbau funktionaler Programme

Im Kern, d.h. wenn man die Modularisierungskonstrukte nicht betrachtet, bestehen funktionale Programme aus:

• der Beschreibung von Werten:

- z.B.(7+23), 30

• Vereinbarung von Bezeichnern für Werte (einschließlich Funktionen):

- val x = 7;

• der Definitionen von Typen:

- type t = ... ; - datatype dt = ... ;

Im Folgenden betrachten wir die Sprache ML.

ML bietet ein interaktives Laufzeitsystem, das Eingaben obiger Form akzeptiert. Selbstverständlich kann man Eingaben auch aus einer Datei einlesen.

Darüber hinaus gibt es Übersetzer für ML.

(6)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 98

Beschreibung von Werten:

- mittels Konstanten oder Bezeichnern für Werte:

23

“Ich bin eine Zeichenreihe“

true x

- durch direkte Anwendung von Funktionen:

abs(~28382)

“Urin“ ^ “stinkt“

not(true)

- durch geschachtelte Anwendung von Funktionen:

floor (~3.4) = trunc (~3.4)

substring (“Urin“ ^ “stinkt“, 0,3)

- durch Verwendung der nicht-strikten Operationen:

if <boolAusdruck> then <Ausdruck>

else <Ausdruck>

<boolAusdruck> andalso <boolAusdruck>

<boolAusdruck> orelse <boolAusdruck>

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 99

Begriffsklärung: (Ausdruck, expression)

Ausdrücke sind das Sprachmittel zur Beschreibung von Werten. Ein Ausdruck (engl. expression) in ML ist

- eine Konstante,

- ein Bezeichner (Variable, Name),

- die Anwendung einer Funktion auf einen Ausdruck, - ein nicht-strikter Ausdruck gebildet mit den

Operationen if-then-else, andalso oder orelse, - oder ist mit Sprachmitteln aufgebaut, die erst

später behandelt werden.

Jeder Ausdruck hat einen Typ:

- Der Typ einer Konstanten ergibt sich aus der Signatur.

- Der Typ eines Bezeichners ergibt sich aus dem Wert, den er bezeichnet.

- Der Typ einer Funktionsanwendung ist der Ergebnistyp der Funktion.

- Der Typ eines if-then-else-Ausdrucks ist gleich dem Typ des Ausdruck im then- bzw. else-Zweig.

Der Typ der anderen nicht-strikten Ausdrücke ist bool.

Präzedenzregeln:

Wenn Ausdrücke nicht vollständig geklammert sind, ist im Allg. nicht klar, wie ihr Syntaxbaum aussieht.

Beispiele:

5 6 7 6 89:;

<=>?; 6 89:; @9;>?; 89:;

<=>?; =AB=>?@ 89:; @9;>?; 89:;

Präzedenzregeln legen fest, wie Ausdrücke zu strukturieren sind:

- Am stärksten binden Funktionsanwendungen in Präfixform.

- Regeln für Infix-Operationen:

CA<C D E FG HG BCIG J@BK

CA<CD L MG NK

CA<CD O 6 PQ P Q P6 Q6K

Je höher die Präzedenzzahl, desto stärker binden die Operationen.

- andalsobindet stärker alsorelseR - Binäre Operationen sind linksassoziativ (d.h.

sie werden von links her geklammert).

- Vergleichsoperationen binden stärker als nicht- strikte Operationen.

Deklaration und Bezeichnerbindung:

Bisher haben wir Ausdrücke formuliert, die sich auf die vordefinierten Funktions- und Konstanten- bezeichner von ML gestützt haben.

Syntaktisch gesehen, heißt Programmierung:

- neue Typen, Werte und Funktionen zu definieren, - die neu definierten Elemente unter Bezeichnern

zugänglich zu machen.

Begriffsklärung: (Vereinbarung, Deklaration)

In Programmiersprachen dienen Vereinbarungen oder Deklarationen (engl. declaration) dazu, den in einem Programm verwendeten Elementen Bezeichner/Namen zu geben.

Dadurch entsteht eine Bindung (n,e) zwischen dem Bezeichner n und dem bezeichneten

Programmelement e.

An allen Programmstellen, an denen die Bindung sichtbar ist, kann der Bezeichner benutzt werden, um sich auf das Programmelement zu beziehen.

(7)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 102

Bemerkung:

• Die Art der Programmelemente, die in Deklarationen vorkommen können, hängt von der Programmiersprache ab. In ML sind es im Wesentlichen Typen und Werte.

• Die Regeln, die die Sichtbarkeit von Bindungen bzw. Bezeichern festlegen, sind ebenfalls sprachabhängig und können sehr komplex sein.

Wir führen die Sichtbarkeitsregeln schrittweise ein.

In Folgenden betrachten wir drei Arten von ML-Vereinbarungen:

1. Wertvereinbarungen

2. Vereinbarungen (rekursiver) Funktionen 3. Vereinbarungen benutzerdeklarierter Typen

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 103

Wertvereinbarungen:

• haben (u.a.) die Form:

val <Bezeichner> = <Ausdruck> ; val <Bezeichner> : <Typ> = <Ausdruck> ; In der ersten Form wird der Typ des Ausdrucks zum Typ des Bezeichners, in der zweiten Form muss der Typ des Ausdrucks gleich dem vereinbarten Typ sein.

• Der rechtsseitige Ausdruck darf nur Bezeichner enthalten, die „vorher“ oder im Ausdruck selbst gebunden wurden.

Beispiele: (Wertvereinbarungen)

val sieben : real = 7.0 ; val dkv = 3.4 ;

val flag = floor ( ~dkv ) = trunc (~dkv) ; val dkv = “Deutscher Komiker Verein e.v.“ ;

Funktionsvereinbarungen:

Zwei Probleme:

1. bisher keine Ausdrücke, die eine Funktion als Ergebnis liefern (Ausnahme: Funktionsbezeichner) 2. Funktionen können rekursiv sein, d.h. der

Funktionsbezeichner kommt im definierenden Ausdruck vor.

Lösungen:

Zu 1.: Erweitere die Menge der Ausdrücke, so dass Ausdrücke Funktionen beschreiben können.

Dann kann die obige Wertvereinbarung genutzt werden.

Zu 2.: Benutze spezielle Syntax für rekursive Funktionsdeklarationen.

Genaueres dazu in Unterabschnitt 3.1.2.

Beispiele: (Funktionsvereinbarungen)

val string2charl:string->char list = explode;

val charl2string = implode ; fun fac(n:int):int =

if n=0 then 1 else n*fac(n-1) ; fun fac(n)= if n=0 then 1 else n*fac(n-1) ;

Typvereinbarungen:

Zwei Probleme:

1. bisher keine Ausdrücke, die Typen als Ergebnis liefern

2. Typen können rekursiv sein, d.h. der vereinbarte Typbezeichner kommt im definierenden Typausdruck vor.

Lösungen:

Zu 1.: Führe Ausdrücke für Typen ein.

Zu 2.: Benutze spezielle Syntax für rekursive Typ- deklarationen.

Genaueres dazu in Unterabschnitt 3.1.4 .

Beispiele: (Typvereinbarungen)

type intpaar = int * int ; type charlist = char list ; type telefonbuch =

(( string * string * string * int ) * string list ) list ; type intNachInt = int -> int ;

val fakultaet : intNachInt = fac ;

(8)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 106

Begriffsklärung: (Bezeichnerumgebung)

Eine Bezeichnerumgebung ist eine Abbildung von Bezeichnern auf Werte (einschl. Funktionen) und Typen, ggf. auch auf andersartige Programmelemente.

Oft spricht man auch von Namensumgebung oder einfach von Umgebung (engl. Environment).

Bemerkung:

Beispiel: (Bezeichnerumgebung)

Die elementaren Datenstrukturen von ML bilden die Standardumgebung für ML-Programme.

• Eine Bezeichnerumgebung wird häufig als Liste von Bindungen modelliert.

• Jede Datenstruktur definiert eine Bezeichner- umgebung.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 107

Begriffsklärung: (funktionales Programm)

Ein funktionales Programm definiert eine Bezeichnerumgebung für die Anwender des Programms.

Interaktive Laufzeitumgebungen für funktionale Programme erlauben das schrittweise Anreichern und Nutzen von Bezeichnerumgebungen.

Beispiel: (funktionales Programm)

val a = 7;

val b = 8;

fun f (x: int):int = x+a;

val a = 20;

val b = true;

val d = f(a);

Frage: Welchen Wert bezeichnetd?

Bemerkung:

Am obigen Beispiel kann man den Unterschied zwischen Wertvereinbarung und Zuweisung an Programmvariable erkennen. Im Folgenden wird er noch deutlicher werden.

Beispiele: (Funktionsabstraktion) 3.1.2 Rekursive Funktionen

Eine Funktion kann man durch einen Ausdruck beschreiben, in dem die Argumente der Funktion durch Bezeichner vertreten sind.

Um deutlich zu machen, welche Bezeichner für Argumente stehen, werden diese deklariert.

Alle anderen Bezeichner des Ausdrucks müssen anderweitig gebunden werden.

Diesen Schritt von einem Ausdruck zu der Beschreibung einer Funktion nennt man Funktionsabstraktion oderλλλλ-Abstraktion.

1. Quadratfunktion:

Ausdruck: x * x Abstraktion: λ x. x * x

ML-Notation: fn x => x * x fn x : real => x * x Vereinbarung eines Bezeichners für die Funktion:

val sq = fn x :real => x * x ;

2. Volumenberechnung eines Kegelstumpfes:

Formel: Sei h die Höhe, r,R die Radien; dann ergibt sich das Volumen V zu

ML-Ausdruck:

(Math.pi *h) / 3.0 * ( sq(r) + r * R + sq( R ) ) Abstraktion:

λ h, r, R . (Math.pi *h)/3 * ( sq(r) + r * R + sq( R ) ) ML-Notation:

fn ( h:real, r:real, R:real ) =>

(Math.pi * h)/3.0 * (sq(r) + r*R + sq(R)) Vereinbarung eines Bezeichners für die Funktion:

val volkegstu = fn ( h, r, R ) =>

(Math.pi * h)/3.0 * (sq(r) + r*R + sq(R)) ; V = π*h * ( r + r * R + R )

3

2 2

(9)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 110

3. Abstraktion über Funktionsbezeichner:

Ausdruck: f ( f (x) ) Abstraktion: λ f, x . f ( f (x) ) ML-Notation:

fn (f: real -> real, x) => f(f(x)) Vereinbarung eines Bezeichners für die Funktion:

val twice = fn (f:real->real,x) => f(f(x)) val erg = twice ( sq, 3.0 ) ;

4. Abstraktion mit Funktion als Ergebnis:

Ausdruck: f ( f (x) ) 1. Abstraktion: λ x . f ( f (x) ) 2. Abstraktion: λ f. (λ x . f ( f (x) ) )

ML-Notation: fn f =>(fn x:real => f(f(x))) Vereinbarung eines Bezeichners für die Funktion:

val appl2 = fn f =>(fn x:real => f(f(x))) ; val pow4 = appl2 ( sq ) ;

val erg = pow4 ( 3.0 ) ;

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 111

Funktionsdeklaration

In ML können Funktionen entweder mittels einer normalen Wertvereinbarung (siehe oben)

oder mit einer speziellen Syntax deklariert werden.

Erster Schritt zur Einführung der speziellen Syntax:

fun <Bezeichner> <Signatur> = <Ausdruck>

and<Bezeichner> <Signatur> = <Ausdruck>

...

and<Bezeichner> <Signatur> = <Ausdruck> ; wobei die Liste der mit dem Schlüsselwort “and“

begonnenen Zeilen leer sein kann.

Zunächst betrachten wir Signaturen der Form:

<Signatur> S ( <Params> ) <TypOpt>

<Params> S ε | <ParamListe>

<ParamListe> S <ParamDekl>

| <ParamDekl> , <ParamListe>

<ParamDekl> S <Bezeichner> <TypOpt>

<TypOpt> S ε

| : <TypAusdruck>

Beispiel: (rekursive Funktionsdekl.)

1. Einzelne rekursive Funktionsdeklaration:

fun rpow ( r: real, n: int ): real = if n = 0 then 1.0

else r * rpow (r,n-1) ; 2. Verschränkt rekursive Funktionsdeklaraion:

fun gerade ( n: int ) : bool = n = 0 orelse ungerade (n-1) and ungerade ( n: int ) : bool =

if n=0 then false else gerade (n-1)

Deklaration rekursiver Funktionen

Begriffsklärung: (rekursive Definition)

Eine Definition oder Deklaration nennt man rekursiv, wenn der definierte Begriff bzw. das deklarierte Programmelement im definierenden Teil verwendet wird.

Bemerkung:

• Rekursive Definitionen finden sich in vielen Bereichen der Informatik und Mathematik, aber auch in anderen Wissenschaften und der nichtwissenschaftlichen Sprachwelt.

• Wir werden hauptsächlich rekursive Funktions- und Datentypdeklarationen betrachten.

Definition: (rekursive Funktionsdekl.)

Eine Funktionsdeklaration heißt direkt rekursiv, wenn der definierende Ausdruck eine Anwendung der definierten Funktion enthält.

Eine Menge von Funktionsdeklarationen heißt verschränkt rekursiv oder indirekt rekursiv (engl. mutually recursive), wenn die Deklarationen gegenseitig voneinander abhängen.

Eine Funktionsdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Funktionsdeklarationen ist.

Begriffsklärung: (rekursive Funktion)

Eine Funktion heißt rekursiv, wenn es rekursive Funktionsdeklarationen gibt, mit denen sie definiert werden kann.

(10)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 114

Bemerkung:

• Die Menge der rekursiven Funktionen ist berechnungsvollständig.

• Rekursive Funktionsdeklarationen können als eine Gleichung mit einer Variablen verstanden werden, wobei die Variable von einem Funktionstyp ist.

Beispiel: (Definition der Fakultätsfunktion) Gesucht ist die Funktion f, die folgende Gleichung für alle n∈Nat erfüllt:

f(n) = if n=0 then 1 else n * f(n-1)

Zur Auswertung von Funktionsanwendungen:

Sei fun f(x) = A[x]

In ML werden Funktionsanwendungen f(e) nach der Strategie call-by-value ausgewertet:

• Werte AusdruckTaus; Ergebnis nennt man den akutellen Parameterz.

• Ersetze x in A[x]durch z.

• Werte den resultierenden AusdruckA[z]aus.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 115

Formen rekursiver Funktionsdeklarationen:

Vereinfachend betrachten wir nur Funktionsdeklara- tionen, bei denen die Fallunterscheidung „außen“ und die rekursiven Aufrufe in den Zweigen der

Fallunterscheidung stehen.

• Eine rekursive Funktionsdeklaration heißt linear rekursiv, wenn in jedem Zweig der Fallunter- scheidung höchstens eine rekursive Anwendung erfolgt (Beispiel: Definition von fac).

• Eine rekursive Funktionsdeklaration heißt repetitiv, wenn sie linear rekursiv ist und die rekursiven Anwendungen in den Zweigen der Fallunterscheidung an äußerster Stelle stehen.

Beispiele:

1. Die übliche Definition von fac ist nicht repetitiv, da im Zweig der rekursiven Anwendung die Multiplikation an äußerster Stelle steht.

2. Die folgende Funktion facrep ist repetitiv:

fun facrep ( n: int, res: int ): int = if n=0 then res

else facrep( n-1, res*n )

• Eine rekursive Funktionsdeklaration für f heißt geschachtelt rekursiv, wenn sie Teilausdrücke der Form f ( ... f ( ... ) ... ) enthält.

• Eine rekursive Funktionsdeklaration für f heißt kaskadenartig rekursiv, wenn sie Teilausdrücke der Form h ( ... f ( ... ) ... f ( ... ) ... ) enthält.

Beispiel: (kaskadenartige Rekursion)

Berechne: Wie viele Kaninchen-Pärchen gibt es nach n Jahren, wenn man

• am Anfang mit einem Pärchen beginnt,

• jedes Pärchen nach zwei Jahren und dann jedes folgende Jahr ein weiteres Pärchen Nachwuchs erzeugt und

• die Kaninchen nie sterben.

Die Anzahl der Pärchen stellen wir als Funktion fib von n dar:

• vor dem 1. Jahr: fib(0) = 1

• nach dem 1. Jahr: fib(1) = 1

• nach dem 2. Jahr: fib(2) = 2

nach dem n. Jahr:

die im Jahr vorher existierenden Pärchen plus die Anzahl der neu geborenen und die ist gleich der Anzahl von vor zwei Jahren, also gilt:

fib( n ) = fib( n-1 ) + fib( n-2 ) für n > 1.

Insgesamt ergibt sich folgende kaskadenartige Funktionsdeklaration:

fun fib ( n: int ):int =

if n<= 1 then 1 else fib (n-1) + fib (n-2)

• Aus Beschreibungssicht spielt die Form der Rekursion keine Rolle; wichtig ist eine möglichst am Problem orientierte Beschreibung.

• Aus Programmierungssicht spielt Auswertungs- effizienz eine wichtige Rolle, und diese hängt von der Form der Rekursion ab.

Beispiel:

Kaskadenartige Rekursion führt im Allg. zu einer exponentiellen Anzahl von Funktionsanwendungen.

( z.B. bei fib(30) bereits 1.664.079 Anwendungen )

Bemerkung:

(11)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 118

3.1.3 Listen und Tupel

Eine Liste über einem Typ T ist eine total geordnete Multimenge mit endlich vielen Elementen aus T

(bzw. eine endliche Folge, d.h. eine Abb. {1,...,n }U T ).

ML stellt standardmäßig eine Datenstruktur für Listen bereit, die bzgl. des Elementtyps parametrisiert ist:

Typen: Ist V ein ML-Typ, dann bezeichnet VWXYZ den Typ der Listen mit Elementen aus V[

Funktionen:

hd : t list \ t tl : t list \ t list

_::_ : t * t list \ t list (* cons *) null : t list \ bool (* Test, ob Liste leer *) length: t list \ int

_@_ : t list * t list \ t list rev : t list \ t list

(* Revertieren/Umkehren einer Liste *) Konstanten:

nil : \ t list

Die Datenstruktur der Listen

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 119

Dem Typ t list ist als Wertemenge die Menge aller Listen über Elementen vom Typ ] zugeordnet.

Bemerkungen:

• In ML gibt es eine spezielle Notation für Listen:

statt a :: a :: ... :: a :: nil kann man [ a ,a , ... ,a ]

schreiben; insbesondere [ ] für nil.

• Selbstverständlich kann man mit Listen von Listen arbeiten.

• Wie wir in 3.3 sehen werden, unterstützt ML Typparameter. Z. B. ist nil vom Typ ‘a list, wobei ‘a eine Typvariable ist.

k k-1 1

k k-1 1

Beispiele: (Funktionen auf Listen)

1. Addiere alle Zahlen einer Liste von Typ int list: - fun mapplus ( xl: int list ) =

if null(xl) then 0

else hd(xl) + mapplus(tl(xl)) ; - mapplus ( [ 1,2,3,4,5,6 ] ) ;

2. Prüfen einer Liste von Zahlen auf Sortiertheit:

- fun ist_sortiert ( xl ) =

if null (xl) orelse null ( tl(xl) ) then true

else if hd (xl) <= hd(tl(xl)) then ist_sortiert( tl (xl)) else false ;

3. Zusammenhängen zweier Listen (engl. append):

- fun append ( l1, l2 ) = if l1 = [ ] then l2

else hd (l1) :: append ( tl(l1), l2 ) ; 4. Umkehren einer Liste:

- fun rev ( xl ) = if xl = nil then [ ]

else append ( rev (tl (xl)), [ hd(xl) ] ) ;

5. Zusammenhängen der Elemente einer Liste von Listen:

- fun concat ( xl ) = if null(xl) then [ ]

else append ( hd(xl) , concat( tl( xl ) ) ) ;

Bemerkungen:

• Rekursive Funktionsdeklaration sind bei Listen angemessen, weil Listen rekursive Datenstrukturen sind.

• Mit Pattern Matching lassen sich die obigen Deklaration noch eleganter fassen (s. unten).

6. Wende eine Liste von Funktionen vom Typ int ^ int nacheinander auf eine ganze Zahl an:

- fun seqappl ( xl : (int^int) list, i: int ) : if null( xl ) then i

else seqappl ( tl(xl), (hd(xl) i)) ;

Die Datenstrukturen der Tupel

Paare oder 2-Tupel sind die Elemente des kartesischen Produktes zweier ggf. verschiedener Mengen oder Typen. Paare gehören zu Produkttypen.

Als Typkonstruktor wird * in Infix-Schreibweise benutzt:

Wir betrachten zunächst Paare und verallgemeinern dann auf n-Tupel:

(12)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 122

Typen: Sindt1, t2 ML-Typen, dann bezeichnet t1 * t2 den Typ der geordneten Paare

aus Elementen von t1 undt2.

Funktionen:

(_,_): t1 * t2 _ t1 * t2 (* Paarbildung *)

#1 : t1 * t2 _ t1 (* 1. Komponente *)

#2 : t1 * t2 _ t2 (* 2. Komponente *) Konstanten: keine

Dem Typ t1*t2 ist die Menge der Paare bestehend aus Elementen vont1 undt2 zugeordnet.

Transformiere eine Liste von Paaren in ein Paar von Listen:

fun unzip ( xl:(‘a*‘b) list ):

(‘a list)*(‘b list) = if null( xl ) then ([],[])

else ( (#1(hd(xl))) :: (#1(unzip(tl(xl)))), (#2(hd(xl))) :: (#2(unzip(tl(xl)))) );

( auch das geht erheblich schöner mit Pattern Matching )

Beispiel: (Funktionen auf Paaren)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 123

ML unterstützt n-Tupel für alle n≥0, n≠1:

Für n=0 ist es die triviale Datenstruktur Unit:

Typ: unit

Funktionen: keine

Konstanten: (): ` unit

Die Wertemenge zum Typunit enthält genau ein Element, genannt Einheit (engl. unity).

Bemerkung:

unitwird oft als Ergebnistyp verwendet, wenn es keine relevanten Ergebniswerte gibt.

Für n=1 gilt:

1-Tupel vom Typ t werden wie Elemente vom Typt betrachtet.

Für n>1 gilt:

n-Tupel sind eine Verallgemeinerung von Paaren.

Klammern werden zur Tupel-Konstruktion verwendet.

#i mit 0<i≤n selektiert die i-te Komponente.

1. Flache ein Paar von Tripeln in ein 6-Tupel aus:

- fun ausflachen ( p: (‘a * ‘b * ‘c) * (‘d * ‘e * ‘f) ) = ( #1(#1(p)), #2(#1(p)), #3(#1(p)),

#1(#2(p)), #2(#2(p)), #3(#2(p)) ) ; 2. Funktion zur Paarbildung:

- fun paarung (lk,rk) = (lk,rk) ;

Beispiel: (Funktionen auf n-Tupeln) Beispiel: (Geschachtelte Tupel)

Mit der Tupelbildung lassen sich „baumstrukturierte“

Werte, sogenannte Tupelterme, aufbauen.

So entspricht der Tupelterm:

((8,true), (("Tupel","sind","toll"),"aha"));

dem Baum:

"Tupel" "sind" "toll"

"aha"

8 true

Stelligkeit von Funktionen:

Formal gesehen, sind alle Funktionen in ML einstellig, haben also nur ein Argument. Die Klammern um das Argument kann man weglassen, also f a statt f(a) . Statt mehrerer Parameter übergibt man ein Tupel von Parametern. Die Tupelklammern kann man natürlich nicht weglassen.

Die Additionsoperation in MLop + nimmt nicht zwei Argumente sondern ein Tupel von Argumenten.

Beispiel:

Statt mehrere Parameter als Tupel zu übergeben, kann man sie auch „nacheinander“ übergeben.

Z. B. hat die Funktion

fun curriedplus m n = m + n ; den Typ

fn : int a (int a int)

Die Auswertung des Ausdruck curriedplus 7 liefert also eine Funktion vom Typ(int a int) .

(13)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 126

Zusammenfassung:

• In ML haben alle Funktionen nur eine Parameter!

• Funktionen mit n-Parametern werden in ML durch einstellige Funktionen ausgedrückt, die ein n-Tupel als Parameter nehmen!

• Die bisher verwendeten Klammern bei einstelligen Funktionen sind also überflüssig.

Syntax der Funktionsanwendung in ML:

<Ausdruck> <Ausdruck>

Funktionstyp zum Funktionstyp passender Parametertyp

Pattern Matching

Pattern Matching meint in diesem Zusammenhang die Verwendung von Mustern zur Vereinfachung von Vereinbarungen/Bezeichnerbindungen.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 127

- fun ist_sortiert x =

if null x orelse null (tl x) then true

else if hd x <= hd (tl x) then ist_sortiert( tl x ) else false ;

- fun append (l1,l2) = if l1 = [ ] then l2

else hd l1 :: append ( tl l1, l2 ) ; - fun concat x =

if null x then [ ]

else append ( hd x, concat ( tl x ) ) ; - fun unzip ( x : (‘a * ‘b) list ): (‘a list) * (‘b list) =

if null x then ([],[])

else ( #1 (hd x) :: (#1 (unzip (tl x))),

#2 (hd x) :: (#2 (unzip (tl x))) ) ;

Beispiel: (Funktionssyntax)

Begriffsklärung: (Konstruktoren)

Konstruktoren in ML sind bestimmte Funktionen:

- die Funktionen zur Tupelbildung

- die Listenfunktion _::_ (daher der Name cons) - benutzerdefinierte Konstruktoren zu rekursiven

Datentypen (siehe 3.1.4).

Begriffsklärung: (ML-Muster, ML-Pattern)

Muster (engl. Pattern) in ML sind Ausdrücke gebildet über Bezeichnern, Konstanten und Konstruktoren.

Alle Bezeichner in einem Muster müssen verschieden sein.

Ein Muster M mit Bezeichnern b , ..., b passt auf einen Wert w (engl. a pattern matches a value w), wenn es eine Substitution der Bezeichner b in M durch Werte v gibt, in Zeichen M[v /b , ... , v /b ], so dass

M[v /b , ... , v /b ] = w

1 k

1 k

i

i 1 k

1 1 k k

Beispiel: (ML-Muster, Passen)

1. (a,b) passt auf (4,5) mit Substitution a=4, b=5.

2. (a,b) passt auf (~47, (true, “dada“)) mit a = ~47, b = (true, “dada“) .

3. x::xs passt auf 7::8::9::nil mit x = 7 und xs = 8::9::nil , d.h. xs = [ 8, 9 ] .

4. x1::x2::xs passt auf 7::8::9::nil mit x1 = 7, x2 = 8, x3 = [ 9 ] .

5. first :: rest passt auf [ “computo“, “ergo“, “sum“ ] mit first = “computo“, rest = [ “ergo“, “sum“ ] 6. ( (8,x) , (y,"aha") ) pass auf

((8,true), ( ("Tupel","sind","toll"), "aha") ) mit x = true und y = ("Tupel","sind","toll")

"aha"

8 x y

(14)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 130

Muster können in ML-Wertvereinbarungen verwendet werden:

val <Muster> = <Ausdruck> ;

Wenn das Muster auf den Wert des Ausdrucks passt undσdie zugehörige Substitution ist, werden die Bezeichner im Muster gemäßσ an die zugehörige Werte gebunden.

Beispiel: (Wertvereinbarung mit Mustern)

- val ( a, b ) = ( 4, 5 );

val a = 4 : int val b = 5 : int - a+b ; val it = 9: int

Muster können in ML-Funktionsdeklarationen verwendet werden:

fun <Bezeichner> <Muster> = <Ausdruck>

...

| <Bezeichner> <Muster> = <Ausdruck> ;

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 131

1. Deklaration von mapplus ohne und mit Mustern:

- fun mapplus (xl: int list) = if null xl

then 0

else (hd xl)+ mapplus (tl xl) ; - fun mapplus [] = 0

| mapplus (x::xl) = x + mapplus xl ; 2. Einige der obigen Funktionsdeklaration mit

Verwendung von Mustern:

- fun ist_sortiert [ ] = true

| ist_sortiert (x::nil) = true

| ist_sortiert (x1::x2::xs) = if x1 > x2 then false

else ist_sortiert (x2::xs ) ; - fun append ([],l2) = l2

| append (x::xs,l2) =

x::append (xs,l2) ;

Beispiel: (Funktionsdeklaration mit Mustern)

3. Verwendung geschachtelter Muster:

- fun unzip (xl:(‘a*‘b)list):

(‘a list)*(‘blist) = if null xl then ([],[])

else ((#1 (hd xl))::(#1 (unzip (tl xl))), (#2 (hd xl))::(#2 (unzip (tl xl))));

- fun unzip [] = ([],[])

| unzip ((x,y)::ps) = (x::(#1 (unzip ps)),

y::(#2 (unzip ps)) );

let- und case-Ausdrücke:

Der Mustermechanismus kann auch innerhalb von Ausdrücken eingesetzt werden.

1. Syntax des let-Ausdrucks:

let <Liste von Deklarationen>

in <Ausdruck>

end

Die aus den Deklarationen resultierenden Bindungen sind nur im let-Ausdruck gültig.

D.h. sie sind sichtbar im let-Ausdruck, an den Stellen, an denen sie nicht verdeckt sind.

Beispiele: (let-Ausdruck)

2. Syntax des case-Ausdrucks:

case <Ausdruck0> of

<Muster1> => <Ausdruck1>

...

| <MusterN> => <AusdruckN>

Bedeutung: Werte Ausdruck0 aus und prüfe der Reihe nach, ob der resultierende Wert auf eines der Muster passt. Passt er auf ein Muster, nehme die entsprechenden Bindungen vor und werte den zugehörigen Ausdruck aus (die Bindungen sind nur in dem zugehörigen Ausdruck gültig).

- val a = let val b = 7*8 in b*b end;

- val a = let val a = 7*8

in let val (a,b) = (a,a+1) in a*b

end end;

- fun unzip [] = ([],[])

| unzip ((x,y)::ps) =

let (xs,ys) = unzip ps in (x::xs,y::ys)

end ;

(15)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 134

- fun ist_sortiert xl = case xl of

[] => true

| (x::nil) => true

| (x1::x2::xs) =>

if x1 > x2 then false

else ist_sortiert (x2::xs) ;

Beispiel: (case-Ausdruck)

Bemerkungen:

• Das Verbot von gleichen Bezeichnern in Mustern hat im Wesentlichen den Grund, dass nicht für alle Werte/Typen die Gleichheitsoperation definiert ist.

- val mal2 = fn x => 2 * x ; - val twotimes = fn x => x+x ; - val (a,a) = ( mal2 , twotimes )

• Offene Frage: Was passiert, wenn keines der angegebenen Muster passt?

b

Abrupte Terminierung.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 135

3.1.4 Benutzerdefinierte Datentypen

Übersicht:

• Vereinbarung von Typbezeichnern

• Deklaration neuer Typen

• Summentypen

• Rekursive Datentypen

Fast alle modernen Spezifikations- und

Programmiersprachen gestatten es dem Benutzer,

„neue“ Typen zu definieren.

Vereinbarung von Typbezeichnern

ML erlaubt es, Bezeichner für Typen zu deklarieren (vgl. Folie 105). Dabei wird kein neuer Typ definiert, sondern nur ein Typ an einen Bezeichner gebunden.

type intpaar = int * int type charlist = char list

type adresse = string * string* string * int type telefonbuch =

( adresse * string list ) list type intNachInt = int c int ;

val fakultaet : intNachInt = fac ;

Beispiele: (Typvereinbarungen)

Typvereinbarungen sind nur Abkürzungen.

Zwei unterschiedliche Bezeichner können den gleichen Typ bezeichnen; z.B.:

- type inttripel = int * int * int;

type inttripel = int * int * int - type date = int * int * int;

type date = int * int * int

- fun kalenderwoche (d:date): int = ... ; val kalenderwoche = fn : date -> int - kalenderwoche (192,45,111111);

Neue Typen werden in ML mit dem datatype-Konstrukt definiert, das im Folgenden schrittweise erläutert wird.

Definition eines neuen Typs und Konstruktors:

datatype<Typbezeichner>=

<Konstruktorbezeichner> of <TypAusdruck>

Deklaration neuer Typen

Die obige Datatypdeklaration definiert:

- einen neuen Typ und bindet ihn an <Typbezeichner>

- eine Konstruktorfunktion, die Werte des durch den Typausdruck beschriebenen Typs in den neuen Typ einbettet. Die Konstruktorfunktion ist injektiv.

(16)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 138

datatype person =

Student of string * string * int * int definiert den neuen Typpersonund den Konstruktor

Student: string*string*int*int -> person

Wir definieren die Selektorfunktionen:

- fun vorname (Student(v,n,g,m)) = v ; val vorname = fn : person -> string

- fun name (Student(v,n,g,m)) = n ; val name = fn : person -> string

- fun geburtsdatum (Student(v,n,g,m)) = g ; val geburtsdatum = fn : person -> int - fun matriknr (Student(v,n,g,m)) = m ; val matriknr = fn : person -> int

Beispiel: (Definition von Typ und Konstruktor)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 139

Jede Datentypdeklaration definiert einen neuen Typ, d.h. insbesondere:

- Werte vom Argumentbereich des Konstruktors sind inkompatibel mit dem neuen Typ;

- Werte strukturgleicher benutzerdefinierter Typen sind inkompatibel.

Beispiele: (Typkompatibilität)

1. Der Typperson ist inkompatibel mit dem Typ string*string*int*int, insbesondere ist vorname (“Niels“,“Bohr“,18851007, 221) nicht typkorrekt.

2. Der Typpersonist inkompatibel mit dem strukturgleichen Typ adresse

datatype adresse =

WohnAdresse of string * string * int * int Insbesondere ist

name (WohnAdresse(“Pfaffenbergstraße“,

“Kaiserslautern“, 27, 67663 ) ) nicht typkorrekt.

Bemerkung:

Summentypen

Bei einer Datentypdeklaration kann man verschiedene Alternativen angeben:

datatype<Typbezeichner>=

<Konstruktorbezeichner1> of <TypAusdruck1>

...

| <KonstruktorbezeichnerN> of <TypAusdruckN>

Den Konstruktor kann man sich als eine Markierung der Werte seines Argumentbereichs vorstellen.

Dabei werden Werte mit unterschiedlicher Markierung als verschieden betrachtet.

Ein Summentyp stellt die disjunkte Vereinigung der Elemente anderen Typen zu einem neuen Typ dar.

Die meisten modernen Programmiersprachen unterstützen die Deklaration von Summentypen.

Summentypen in ML:

Beispiele: (Summentypen)

1. Ein anderer Datentyp zur Behandlung von Personen:

datatype person2 =

Student of string*string*int*int

| Mitarbeiter of string*string*int*int

| Professor of string*string*int*int*string

2. Eine benutzerdefinierte Datenstruktur für Zahlen:

- datatype number = Intn of int

| Realn of real

- fun isInt (Intn m) = true

| isInt (Realn r) = false ; val isInt = fn : number -> bool - fun isReal (Intn m) = false

| isReal (Realn r) = true ; val isReal = fn : number -> bool - fun neg (Intn m) = Intn (~m)

| neg (Realn r) = Realn (~r);

val neg = fn : number -> number

(17)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 142

- fun plus (Intn m,Intn n) = Intn (m+n)

| plus (Intn m,Realn r) = Realn((real m)+r)

| plus (Realn r,Intn m) = Realn(r+(real m))

| plus (Realn r,Realn q)= Realn(r+q);

val plus = fn : number * number -> number

Begriffsklärung:

Konstruktorfunktionen oder Konstruktoren liefern Werte des neu definierten Datentyps. Sie können in Mustern verwendet werden (z.B.: Student, Intn).

Diskriminatorfunktionen oder Diskriminatoren prüfen, ob der Wert eines benutzerdefinierten Datentyps zu einer bestimmten Alternative gehört (Beispiel: isInt).

Selektorfunktionen oder Selektoren liefern

Komponenten von Werten des definierten Datentyps (z.B.: vorname, name, ...).

Bemerkung:

In funktionalen Sprachen kann man meist auf Selektorfunktionen verzichten. Man wendet statt- dessen Pattern.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 143

Beispiele: (Aufzählungstypen)

- datatype wochentag =

Montag | Dienstag | Mittwoch | Donnerstag

| Freitag | Samstag | Sonntag ; - fun istMittwoch Mittwoch = true

| istMittwoch w = false ; Oder knapper:

- fun istMittwoch w = (w=Mittwoch) ; Das datatype-Konstrukt kann auch verwendet werden, um Aufzählungstypen zu definieren. Dabei wird in den Alternativen der Teil beginnend mit dem Schlüsselwortof weggelassen.

Die Wertemenge eines Aufzählungstyps ist eine endliche Menge (von Namen).

Weitere Formen des datatype-Konstrukts:

Beide Formen der Datentypdeklaration können kombiniert werden; z.B.:

- datatype intBottom = Bottom

| Some of int

Datentypdeklarationen lassen sich parametrisieren.

Die Typparameter werden dabei vor den vereinbarten Typbezeichner gestellt:

- datatype ‘a bottom = Bottom | Some of ‘a

ML sieht dafür standardmäßig den folgenden Typ vor:

- datatype ‘a option = NONE | SOME of ‘a

Zur Erweiterung von Summentypen

Im Gegensatz zur objektorientierten Programmierung (vgl. Kapitel 5) lassen sich die Summentypen in der funktionalen Programmierung meist nicht erweitern, d.h. einem definierten Datentyp können im Nachhinein keine Alternativen hinzugefügt werden.

Beispiel: (Erweiterung von Summentypen)

Den Datentyp person2:

datatype person2 =

Student of string*string*int*int

| Mitarbeiter of string*string*int*int

| Professor of string*string*int*int*string hätte man ausperson durch Hinzufügen der

Alternativen für Mitarbeiter und Professoren gewinnen können.

Bemerkung:

Die Möglichkeit, einen Datentypen T nachträglich erweitern zu können, bringt zwei Vorteile für die Wiederverwendung mit sich:

1. Der Programmieraufwand wird reduziert.

2. Programme, die für T entwickelt wurden, können teilweise weiter benutzt werden.

In ML müssen Erweiterungen im Allg. durch neue Summentypen realisiert werden.

(18)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 146

Beispiel:

Ein Datentyp dt : datatype dt =

Constr1 of t1

| Constr2 of t2

| Constr3 of t3

| Constr4 of t4

soll eine fünfte Alternative bekommen. Statt alle existierenden Programme auf fünf Alternativen umzustellen, kann man mit dtneuarbeiten:

datatype dtneu =

Constr0 of dt

| Constr5 of t5

Nachteil: Die Programme müssen auf dtneu angepasst werden.

Bemerkung:

Der Datentypexnist in ML erweiterbar. Jede Ausnahme-Vereinbarung fügt eine Alternative hinzu.

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 147

Rekursive Datentypen

Von großer Bedeutung in der Programmierung sind rekursive Datentypen.

Definition: (rekursive Datentypdeklaration)

Eine Datentypdeklaration heißt direkt rekursiv, wenn der definierte Typ in einer der Alternativen der Datentypdeklaration vorkommt.

Wie bei Funktionen gibt es auch verschränkt rekursive Datentypdeklarationen.

Eine Datentypdeklaration heißt rekursiv, wenn sie direkt rekursiv ist oder Element einer Menge verschränkt rekursiver Datentypdeklarationen ist.

Begriffsklärung: (rekursiver Datentyp)

Ein Datentyp heißt rekursiv, wenn er mit einer rekursiven Datentypdeklaration definiert wurde.

Beispiele: (Listendatentypen)

1. Ein Datentyp für Integer-Listen:

- datatype intlist = nil

| Cons of int * intlist ; 2. Ein Datentyp für homogene Listen mit Elementen

von beliebigem Typ:

- datatype ‘a list = nil

| Cons of ‘a * ‘a list Vorsicht:

Diese Deklaration verdeckt den vordefinierten Datentyp‘a list.

3. Der vordefinierte Datentyp für homogene Listen mit Elementen von beliebigem Typ:

- datatype ‘a list = nil

| :: of ‘a * ‘a list

Begriffsklärung: (zu Bäumen) Baumartige Datenstrukturen:

• In einem endlich verzweigten Baum hat jeder Knoten endlich viele Kinder.

• Üblicherweise sagt man, die Kinder sind von links nach rechts geordnet.

•. Einen Knoten ohne Kinder nennt man Blatt, einen Knoten mit Kindern einen inneren Knoten oder Zweig.

• Den Knoten ohne Elter nennt man Wurzel.

Ottmann, Widmayer:

„Bäume gehören zu den wichtigsten in der Informatik auftretenden Datenstrukturen“.

A

B

C

D E

F G

H

(19)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 150

• Ein Baum heißt markiert, wenn jeder Knoten k eine Markierung m(k) besitzt.

• In einem Binärbaum ist jeder Knoten entweder ein Blatt, oder er hat genau zwei Kinder.

• Zu jedem Knoten gehört ein Unterbaum.

Datentyp für markierte Binärbaume:

- datatype intbbaum = Blatt of int

| Zweig of int * intbbaum * intbbaum ; - val einbaum = Zweig (7,

Zweig(3,(Blatt 2),(Blatt 4)), (Blatt 5));

- fun m (Blatt n) = n

| m (Zweig (n,lk,rk)) = n ;

Ein mit ganzen Zahlen markierter Binärbaum heißt sortiert, wenn für alle Knoten k gilt:

- Die Markierungen der linken Nachkommen von k sind kleiner als m(k).

- Die Markierungen der rechten Nachkommen von k sind größer als m(k).

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 151

Aufgabe:

Prüfe, ob ein Baum vom Typintbbaumsortiert ist.

Idee:

Berechne zu jedem Unterbaum die minimale und maximale Markierung und prüfe rekursiv die

Sortiertheitseigenschaft für alle Knoten/Unterbäume.

fun maxmark (Blatt n) = n

| maxmark (Zweig (n,l,r)) =

Int.max(n,Int.max(maxmark l, maxmark r)) fun minmark (Blatt n) = n

| minmark (Zweig (n,l,r)) =

Int.min(n,Int.min(minmark l, minmark r)) fun istsortiert (Blatt n) = true

| istsortiert (Zweig (n,l,r)) =

istsortiert l andalso istsortiert r andalso

(maxmark l)<n andalso n<(minmark r) istsortiert einbaum

Wenig effiziente Lösung!! Besser ist es, die Berechnung von Minima und Maxima mit der Sortiertheitsprüfung zu verschränken.

Idee:

Entwickle eine Einbettung, die für sortierte Bäume auch Minimum und Maximum liefert.

fun istsorteinbett (Blatt n) = (true,n,n)

| istsorteinbett (Zweig (n,l,r)) =

let val (lerg,lmn,lmx) = istsorteinbett l val (rerg,rmn,rmx) = istsorteinbett r in ( lerg andalso rerg andalso

lmx<n andalso n<rmn, lmn, rmx ) end ;

fun istsortiert b = #1 (istsorteinbett b)

Parametrische markierte Binärbaumtypen:

1. Alle Markierungen sind vom gleichen Typ:

datatype ‘a bbaum = Blatt of ‘a

| Zweig of ‘a * ‘a bbaum * ‘a bbaum

2. Blatt- und Zweigmarkierungen sind möglicherweise von unterschiedlichen Typen:

datatype (‘a,‘b) bbaum = Blatt of ‘a

| Zweig of ‘b * (‘a,‘b) bbaum * (‘a,‘b) bbaum

Bäume mit variabler Kinderzahl:

Bäume mit variabler Kinderzahl lassen sich realisieren:

- durch mehrere Alternativen für Zweige (begrenzte Anzahl von Kindern)

- durch Listen von Unterbäumen:

datatype ‘a baum = Kn of ‘a * (‘a baum list) fun zaehlekn (Kn (_,xs)) =

foldr op+ 0 (map zaehlekn xs) Beachte:

Der Rekursionsanfang ergibt sich durch Knoten mit leerer Unterbaumliste.

(20)

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 154

Bäume mit variabler Kinderzahl lassen sich z.B. zur Repräsentation von Syntaxbäumen verwenden, indem das Terminal- bzw. Nichtterminalsymbol als Markierung verwendet wird.

Besser ist es allerdings, die Information über die Symbole mittels Konstruktoren auszudrücken.

Dadurch gewinnt man an Typsicherheit (vgl. nächstes Beispiel).

Verschränkte Datentypdeklarationen:

Genau wie Funktionsdeklarationen werden verschränkt rekursive Datentypdeklarationen mit dem Schlüsselwort „and“ verbunden:

datatype<Typbezeichner1>=

<Konstruktorbezeichner1_1> of <TypAusdruck1_1>

...

| <Konstruktorbezeichner1_N> of <TypAusdruck1_N>

and

ddd

and <TypbezeichnerM>=

<KonstruktorbezeichnerM_1> of <TypAusdruckM_1>

...

| <KonstruktorbezeichnerM_K> of <TypAusdruckM_K>

31.10.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 155

datatype

programm = Prog of wertdekl list * ausdruck and

wertdekl = Dekl of string * ausdruck and

ausdruck =

Bzn of string

| Zahl of int

| Add of ausdruck * ausdruck

| Mul of ausdruck * ausdruck Das Programm

val a = 73 ; ( a + 12 ) ;

Wird durch folgenden Baum repräsentiert:

Prog ( [Dekl (“a“,Zahl 73)],

Add (Bzn “a“,Zahl 12) ) Die Baumrepräsentation eignet sich besser als die Zeichenreihenrepräsentation zur weiteren Verar- beitung von Programmen.

Beispiel: (verschränkte Datentypen)

Der abstrakte Syntaxbaum eines Programms repräsentiert die Programmstruktur unter Verzicht auf Schlüsselworte und Trennzeichen.

Rekursive Datentypen eignen sich sehr gut zur Beschreibung von abstrakter Syntax. Als Beispiel betrachten wir die abstrakte Syntax von Femto:

Unendliche Datenobjekte:

Eine nicht-leere Liste ist ein Wert, zu dem man sich das erste Element und eine Liste als Rest geben lassen kann.

Hat die endliche Liste xl die Länge n>0, dann hat (tl xl) die Länge n-1.

Eine unendliche Liste besitzt keine natürlichzahlige Länge und wird durch Anwendung von tl nicht kürzer.

Unendliche Listen, häufig als Ströme (engl. stream) bezeichnet, sind die einfachsten unendlichen Daten- Objekte.

In ML lassen sich unendliche Listen mit folgender Datenstruktur realisieren:

datatype ‘a stream = Nil

| Cons of ‘a * ( unit -> ‘a stream ) fun hd (Cons ( x, xf )) = x

| hd Nil = raise Empty

fun tl (Cons ( x, xf )) = xf ()

| tl Nil = raise Empty

Beispiel: (unendliche Liste)

Liste aller (positiven) Dualzahlen dargestellt als Zeichenreichen:

- fun incr (#"O"::xs) = #"L"::xs

| incr (#"L"::xs) = #"O":: incr xs

| incr [ ] = [ #"L" ] ; val incr = fn : char list -> char list - fun incrstr s =

implode (rev (incr (rev (explode s))));

val incrstr = fn : string -> string - fun dualz_ab s =

Cons (s, fn x=> dualz_ab (incrstr s));

val dualz_ab = fn: string -> string stream - val dl = dualz_ab "O" ;

val dl = Cons ("O",fn) : string stream - hd dl;

val it = "O" : string - val dl = tl dl;

val dl = Cons ("L",fn) : string stream - val dl = tl dl;

val dl = Cons ("LO",fn) : string stream

Referenzen

ÄHNLICHE DOKUMENTE

Wir haben bisher Relationen betrachtet, die entweder primitiv rekursiv oder rekursiv entscheidbar waren... Dann ist f (~ x) = µy.S~ xy berechenbar und hat als Definitionsbereich

Aufgabe 1: (Methoden zur Berechnung der Resultanten) In der Vorlesung wurde die Resultante auf zwei Weisen berechnet.. Setzen Sie beide Methoden in

Implementieren Sie eine zweite Version mit den Rekursionsformeln aus Satz 7.38 der Vorle- sung.. Arbeiten Sie dabei iterativ, nicht wie in der

Technische Universit¨ at Graz WS 2021/2022. Institut f¨ ur Angewandte Mathematik

Elliptische Kurven und Kryptographie 20.04.2015.

Schreiben Sie ein Programm, welches zu vorgegebenen Daten eine normalverteilte St¨ orung addiert und diese Daten dann gl¨ attet und stellen Sie die Daten, die gest¨ orten Daten und

Dies bedeutet, dass add eine Funktion ist, die einen Int als Argument erh¨ alt und eine Funktion liefert, die noch einen Int als Argument erh¨ alt.... Funktionen h¨

Wir wollen Int durch Nat ersetzen, sodass das Alter nicht mehr negativ