• Keine Ergebnisse gefunden

3. Funktionales Programmieren

N/A
N/A
Protected

Academic year: 2022

Aktie "3. Funktionales Programmieren"

Copied!
58
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

• Grundkonzepte funktionaler Programmierung

• Algorithmen auf Listen und Bäumen

• Abstraktion mittels Polymorphie und Funktionen höherer Ordnung

• Semantik, Testen und Verifikation

3. Funktionales

Programmieren

(2)

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.1.5 Felder in ML

3.1.6 Signaturen und Strukturen

3.2 Algorithmen auf Listen und Bäumen 3.2.1 Sortieren

3.2.2 Suchen

3.3 Abstraktion mittels Polymorphie und Funktionen höherer Ordnung

3.3.1 Typisierung

3.3.2 Funktionen höherer Ordnung 3.4 Semantik, Testen und Verifikation

3.4.1 Zur Semantik funktionaler Programme 3.4.2 Testen und Verifikation

4. Prozedurales Programmieren

Inhalt von Kapitel 3:

(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

(4)

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.

Definition: (partielle Funktion)

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

Andernfalls heißt sie total.

(5)

• 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?

+

+ +

Bemerkungen:

• Entsprechendes gilt in anderen Programmier- paradigmen.

(6)

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. (z.B. in der objektorientierten Programmierung: „immutable objects“)

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

Bemerkungen:

(7)

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

(8)

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.

(9)

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.

(10)

• 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:

(11)

Die Datenstruktur Boolean:

Typ: bool Funktionen:

= : bool * bool Æ bool

<> : bool * bool Æ bool not: bool Æ bool

Konstanten:

true: bool false: bool

Dem Typbezeichner bool ist die Wertemenge { true, false } zugeordnet.

= bezeichnet die Gleichheit auf Wahrheitswerten

<> bezeichnet die Ungleichheit auf Wahrheitsw.

not bezeichnet die logische Negation.

true bezeichnet den Wert true.

false bezeichnet den Wert false.

(12)

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.

(13)

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 int 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 definiertt.

Insbesondere bezeichnen +, *, abs, ~ partielle Funktionen.

(14)

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)

(15)

< : 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: 0.0, 1000.0, 128.9, ~2.897 - mit Exponenten: 0e0, 1E3, 1289E~1, ~2897e~3 Dem Typbezeichner real 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.

(16)

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 \t, \n, \\, \“, ... hat,

- die Form \zzz hat, wobei z eine Ziffer ist und die

dargestellte Dezimalzahl ein legaler Zeichencode ist.

(17)

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 Æ bool

<>: string * string Æ bool

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

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

<=: string * string Æ bool

>=: string * string Æ bool

^ : string * string Æ string

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

(* Teilzeichenreihe *) str: char Æ string (* entsprechende

Zeichenreihe der Länge 1 *)

(18)

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 Nach Zeilenumbruch“

Dem Typbezeichner string 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).

(19)

• 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 Datenstruktur string und der Listendatenstruktur stellt ML auch die

Funktionen

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

(20)

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.

(21)

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>

(22)

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.

(23)

Präzedenzregeln:

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

Beispiele:

3 = 5 = true

false = true orelse true false andalso true orelse true

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

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

- Regeln für Infix-Operationen:

infix 7 *, /, div, mod;

infix 6 +, -;

infix 4 = <> < > <= >=;

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

- andalso bindet stärker als orelse.

- Binäre Operationen sind linksassoziativ (d.h.

sie werden von links her geklammert).

- Vergleichsoperationen binden stärker als nicht-

(24)

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.

(25)

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

(26)

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.“ ;

(27)

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

(28)

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 ;

(29)

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.

(30)

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 bezeichnet d?

Bemerkung:

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

(31)

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 ;

(32)

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 = * ( r + r * R + R )π*h

3

2 2

(33)

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

(34)

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> Æ ( <Params> ) <TypOpt>

<Params> Æ ε | <ParamListe>

<ParamListe> Æ <ParamDekl>

| <ParamDekl> , <ParamListe>

<ParamDekl> Æ <Bezeichner> <TypOpt>

<TypOpt> Æ ε

(35)

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.

(36)

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.

(37)

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 Ausdruck e aus; Ergebnis nennt man den akutellen Parameter z .

• Ersetze x in A[x] durch z .

• Werte den resultierenden Ausdruck A[z] aus.

(38)

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 )

(39)

• 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

(40)

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:

(41)

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 } Æ T ).

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

Typen: Ist t ein ML-Typ, dann bezeichnet t list den Typ der Listen mit Elementen aus t.

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

(42)

Dem Typ t list ist als Wertemenge die Menge aller Listen über Elementen vom Typ t 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 ] ) ;

(43)

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

(44)

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:

(45)

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

aus Elementen von t1 und t2.

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 von t1 und t2 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)

(46)

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 Typ unit enthält genau ein Element, genannt Einheit (engl. unity).

Bemerkung:

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

Für n=1 gilt:

1-Tupel vom Typ t werden wie Elemente vom Typ t 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.

(47)

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

(48)

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 ML op + 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 Æ (int Æ int)

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

(49)

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.

(50)

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

(51)

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

(52)

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

(53)

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> ;

(54)

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)

(55)

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

(56)

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)

(57)

- 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?

Î Abrupte Terminierung.

(58)

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.

Referenzen

ÄHNLICHE DOKUMENTE

Er besagt, dass jede stetige Funktion einen kleinsten Fixpunkt hat und zeigt sogar, wie man diesen erh¨ alt..

Allerdings terminiert g c nicht, weil es sein Argument so weit auswerten muss, bis klar ist, welcher Wertkonstruktor (hier M a k e ) angewandt wurde. Wir ben¨ otigen also nicht

Wir werden hierbei einige Einschr¨ ankungen vornehmen, die allerdings keine wirklichen Einschr¨ ankungen sind, da man alle anderen Haskell-Programme in unsere erlaubte Syntax

Wir m¨ ussten eigentlich noch zeigen, dass alle Funktionen, die wir in der Definition der Semantik benutzt haben, auch stetig sind.. Da dies allerdings sehr aufw¨ andig ist, m¨

Intuitiv geschieht dies, indem man Typvariablen so ersetzt, dass alle Typgleichungen von der Form τ = τ sind, also zum Beispiel α = Int wird zu Int = Int, indem man α durch

I ” nicht l¨ osbar“ liefert, wenn es keine L¨ osung f¨ ur E gibt, I und andernfalls eine allgemeinste L¨ osung f¨ ur E liefert.. Wir starten mit einer Substitution s, die am

Die folgenden Fragen sollte man sich selbst beim Lernen stellen und darauf achten, dass man diese auch formal beantworten kann?. Die Liste hat keinen An- spruch auf

[r]