• Keine Ergebnisse gefunden

3.4Semantik, Testen & VerifikationÜbersicht:•Einführung in Semantik von Programmiersprachen •Testen und Verifikation

N/A
N/A
Protected

Academic year: 2022

Aktie "3.4Semantik, Testen & VerifikationÜbersicht:•Einführung in Semantik von Programmiersprachen •Testen und Verifikation"

Copied!
11
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 226

3.4 Semantik, Testen & Verifikation

Übersicht:

• Einführung in Semantik von Programmiersprachen

• Testen und Verifikation

3.4.1 Zur Semantik funktionaler Programme

Lernziele in diesem Unterabschnitt:

- Was bedeutet Auswertungssemantik?

- Wie sieht sie im Falle von ML aus?

- Welche Bedeutung haben Bezeichner- umgebungen dabei?

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 227

In erster Näherung definiert eine Funktionsdeklaration eine partielle Funktion. Gründe für Partialität:

1. Der Ausdruck, der die Funktion definiert, ist bereits partiell:

- fun division (dd,dr) = dd / dr ; - fun hd x::xs = x ;

2. Behandlung rekursiver Deklarationen:

a. Insgesamt unbestimmt:

- fun f x = f x ;

b. Teilweise unbestimmt (hier für negative Zahlen):

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

Ziel:

Ordne jeder syntaktisch korrekten Funktionsdeklaration eine partielle Funktion zu. Die Semantik beschreibt diese Zuordnung.

Wir unterscheiden hier denotationelle und operationelle Semantik. Statt operationeller Semantik spricht man häufig von Auswertungssemantik.

Eine Semantik, die jeder Funktionsdeklaration explizit eine partielle Funktion als Bedeutung zuordnet, d.h. eine Abbildung von Funktions- deklarationen auf partielle Funktionen definiert, nennen wir denotationell.

Begriffsklärung: (denotationelle Semantik)

Eine denotationelle Semantik würde der obigen Funktionsdeklaration vonfaceine Funktion f

f: Int Int zuordnen, wobei

Int = { x | x ist Wert von Typ int } ∪ { } Diese Funktion muss die Gleichung fürfacerfüllen.

Das Symbol steht dabei für „undefiniert“ und wird häufig als bottom bezeichnet.

Beispiel: (denotationelle Semantik)

⊥ ⊥

Zwei mögliche Lösungen:

⊥ , falls k =⊥oder k < 0 f (k) =

k! , sonst

⊥ , falls k =⊥ f (k) = 0 , k < 0

k! , sonst

Wir zeigen, dass f eine Lösung der Gleichung ist:

n=⊥: links: f (⊥) =⊥

rechts: if⊥=0 then 1 else⊥* f (⊥-1) =⊥ 1

2

2

2

2 n<0: links: f (n) = 0

rechts: if n=0 then 1 else n*f (n-1) = n*0 = 0 n≥0: links: f (n) = n!

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

= n*(n-1)! = n!

Genauso läßt sich zeigen, dass f eine Lösung ist.

2

2

1 2

2

(2)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 230

Die denotationelle Semantik muss sicherstellen,

• dass es für jede Funktionsdeklaration mindestens eine Lösung gibt, und

• eine Lösung auszeichnen, wenn es mehrere gibt.

Üblicherweise wählt man die Lösung, die an den wenigsten Stellen definiert ist, und betrachtet nur strikte Funktionen als Lösung:

Begriffsklärung: (strikte Funktionen)

Eine n-stellige Funktion oder Operation heißt strikt, wenn sie⊥ als Ergebnis liefert, sobald eines der Argumente⊥ ist.

Beispiele: (nicht-strikte Funktionen)

if-then-else, andalso, orelse sind nicht-strikte Funktionen/Operationen.

Bemerkungen:

• Denotationelle Semantik basiert auf einer Theorie partieller Funktionen und Fixpunkttheorie.

- Vorteile: Für Beweise besser geeignet.

- Nachteil: Theoretisch aufwendiger zu handhaben.

• ⊥ steht für undefiniert, unabhängig davon, welcher der Gründe für Partialität vorliegt.

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 231

Eine Semantik, die erklärt, wie eine Funktions- deklaration auszuwerten ist, nennen wir operationell oder Auswertungssemantik.

Begriffsklärung: (operationelle Semantik)

Begriffsklärung: (Auswertungsstrategie)

Die Auswertungsstrategie legt fest,

- in welchen Schritten die Ausdrücke ausgewertet werden und

- wie die Parameterübergabe geregelt ist.

Wir erläutern

• eine Auswertungsstrategie für funktionale Programme,

• welche Rolle Bezeichnerumgebungen dabei spielen,

• und führen wichtige Begriffe ein.

Begriffsklärung: (formaler/aktueller Parameter)

Ein Bezeichner, der in einer Funktionsdeklaration einen Parameter bezeichnet, wird formaler Parameter genannt.

Der Ausdruck oder Wert, der einer Funktion bei einer Anwendung übergeben wird, wird aktueller Parameter genannt.

Beispiele: (Parameterübergabeverfahren)

Parameterübergabe:

1. Call-by-Value:

- Werte die aktuellen Parameter aus.

- Benutze die Ergebnisse anstelle der formalen Parameter im definierenden Ausdruck/Rumpf.

- Werte den Rumpf aus.

2. Call-by-Name:

- Ersetze alle Vorkommen der formalen Parameter durch die (unausgewerteten) aktuellen Parameterausdrücke.

- Werte den Rumpf aus.

Unterschiedliche Auswertungsstrategien führen im Allg. zu unterschiedlichen Ergebnissen.

Betrachte:

fun f ( x, y ) = if x=0 then 1 else f( x-1, f(x-y,y)) ; Werte den Ausdruck f(1,0) aus:

Beispiel: (Auswertungsstrategien)

1. Call-by-Value:

f ( 1, 0 )

=

if 1=0 then 1 else f( 1-1, f(1-0,0))

=

if false then 1 else f( 1-1, f( 1-0,0) )

=

f( 1-1, f( 1-0, 0 ) )

=

f( 0, f( 1-0, 0 ) )

=

f( 0, f( 1-0, 0 ) )

=

f( 0, f( 1, 0 ) )

=

f( 0, if 1=0 then 1 else f( 1-1, f(1-0,0)) )

= ....

=

f( 0, f( 0, f( 1, 0 ) ) )

= ....

Diese Auswertung kommt nicht zum Ende, d.h. sie terminiert nicht.

(3)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 234

2. Call-by-Name:

f ( 1, 0 )

=

if 1=0 then 1 else f( 1-1, f(1-0,0))

=

if false then 1 else f( 1-1, f( 1-0,0) )

=

f( 1-1, f( 1-0, 0 ) )

=

if 1-1=0 then 1

else f(1-1 -1, f(1-1- f( 1-0, 0 ), f( 1-0, 0 ) ))

=

if true then 1

else f(1-1 -1, f(1-1- f( 1-0, 0 ), f( 1-0, 0 ) ))

= 1

Mit Call-by-Name terminiert die Auswertung von f(1,0).

Begriffsklärung: (Normalform)

Der Ergebnisausdruck einer terminierenden Auswertung wird Normalform genannt.

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 235

Informelle Auswertungssemantik von ML

ML benutzt Call-by-Value Parameterübergabe.

Die Ausdrücke werden

- links nach rechts (engl. leftmost) und - von innen nach außen (engl. innermost) ausgewertet.

Die Auswertung eines Ausdrucks A kann in ML - zu einem Ergebnis führen, dem Wert von A, - eine Ausnahme auslösen,

- nicht terminieren.

Beispiel: (Ausnahmen)

• Die Auswertung des Ausdrucks 8 div 0 löst die Ausnahme „divide by zero“ aus.

• Die Anwendung der Funktion head auf die leere Liste löst die Ausnahme „non exhaustive match failure“

aus:

- fun head ( x::xs ) = x;

- head [ ]

Ausdrücke ohne benutzerdefinierte Funktionen:

Wir betrachten zunächst Ausdrücke, die nur über - Konstante,

- Bezeichner für Werte von Datentypen, - Bezeichner für vordefinierte Funktionen, - sonstige Ausdruckskonstrukte von ML

aufgebaut sind und beschreiben die Auswertung informell.

Sei E eine Bezeichnerumgebung, die die Bindungen für die Bezeichner eines Ausdrucks A enthält.

Die Auswertung von A in E wird rekursiv über den Aufbau von A beschrieben:

• Ist A eine Konstante K, dann liefere den Wert von K.

• Ist A ein Bezeichner für den Wert w eines Datentyps, dann liefere w.

• Ist A eine Funktionsanwendung einer vordefinierten Funktion, dann

- werte die aktuellen Parameterausdrücke aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls wende die vordefinierte Funktion auf die aktuellen Parameter an;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls liefere das Ergebnis.

• Ist A ein if-then-else-Ausdruck, dann - werte zunächst die Bedingung aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls werte je nach Ergebnis den then- oder else-Zweig aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls liefere das Ergebnis der Auswertung des betrachteten Zweiges.

• Ist A ein andalso- oder orelse-Ausdruck:

analog zu if-then-else

(4)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 238

• Ist A ein case-Ausdruck, dann

- werte zunächst den Ausdruck nach dem Schlüsselwortaus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls prüfe der Reihe nach, auf welches Muster das Ergebnis passt;

- passt es auf ein Muster, werte den zugehörigen Ausdruck aus;

> wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

> andernfalls liefere das Ergebnis;

- passt es auf kein Muster, löse die Ausnahme

„non exhaustive match failure“ aus.

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 239

• Ist A ein let-Ausdruck,let DL in B end, dann - werte DL aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls erweitert die Auswertung von DL die Umgebung E zu E1;

- werte B in E1 aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls liefere das Ergebnis der Auswertung von B.

• Terminiert einer der Teilauswertungen nicht, dann terminiert die Gesamtauswertung auch nicht.

Funktionsabschlüsse:

Kernfragen:

Wie wird das Ergebnis eines funktionswertigen Ausdrucks dargestellt?

Wie wird eine benutzerdeklarierte Funktion in der Bezeichnerumgebung dargestellt?

Beispiel:

val a = 5;

val b = 8;

fun f (x: int):int = (x+a) * b ; val a = ~4;

val d = f a;

Was soll für die Funktion f eingetragen werden:

Das Ergebnis eines funktionswertigen Ausdrucks wird durch ein Paar ( (x,AR), FE ) dargestellt, den sogenannten Funktionsabschluss (engl. Closure):

- x bezeichnet den formalen Parameter;

- AR bezeichnet den Ausdruck, der den Funktions- rumpf beschreibt.

- FE bezeichnet die aktuell gültige Umgebung.

Beispiel:

Bezeichnerumgebung jeweils nach Auswertung einer Vereinbarung im obigen Programmbeispiel:

[ (a,5) ] [ (b,8) , (a,5) ]

[ (f, ( (x,(x+a)*b), [ (b,8) , (a,5) ] ) ) , (b,8) , (a,5) ] [ (a,~4) , (f, ( (x,(x+a)*b) , [ (b,8) , (a,5) ] ) ) , (b,8) ] [ (d,8), (a,~4) , (f, ( (x,(x+a)*b), [(b,8),(a,5) ]) ) , (b,8) ]

Ausdrücke mit benutzerdefinierten Funktionen:

Auswertung der Anwendung von benutzerdefinierten Funktionen, d.h. von Ausdrücken A der Form:

<funktionswertiger Ausdruck> <Parameterausdruck>

- werte zunächst den funktionswertigen Ausdruck aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls liefert die Auswertung einen Funktionsabschluss ( (x,AR), FE );

- werte den Parameterausdruck aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls liefert die Auswertung den Wert w des aktuellen Parameters;

- werte den Ausdruck AR in der Umgebung (x,w)::FE aus;

- wird dabei eine Ausnahme ausgelöst, löst die Auswertung von A die gleiche Ausnahme aus;

- andernfalls ist das Ergebnis der Auswertung von AR in (x,w)::FE das Ergebnis der Auswertung von A.

(5)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 242

Beispiel: (Auswertung von Funktionsanw.)

Wir betrachten die Funktionsanwendung (f a) in der aktuellen Umgebung (vgl. obiges Beispiel):

[ (a,~4) , (f, ( (x,(x+a)*b), [ (b,8) , (a,5) ] ) ) , (b,8) ]

1. Die Auswertung von f in der aktuellen Umgebung liefert den Funktionsabschluss:

( ( x, (x+a)*b) , [ (b,8) , (a,5) ] )

2. Die Auswertung von a in der aktuellen Umgebung liefert den Wert ~4 .

3. Also ist (x+a)*b in [ (x,~4), (b,8), (a,5) ] auszuwerten:

- Auswertung von x liefert ~4 . - Auswertung von a liefert 5 .

- Auswertung von + liefert vordefinierte Funktion, deren Anwendung auf ~4 und 5 liefert den Wert 1.

- Auswertung von b liefert 8.

- Auswertung von * liefert vordefinierte Funktion, deren Anwendung auf 1 und 8 liefert den Wert 8.

Insgesamt liefert die Auswertung von (f a) in der aktuellen Umgebung also den Wert 8.

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 243

Bemerkung:

• Im Zusammenhang mit Pattern Matching ist die Bindung der aktuellen Parameter etwas aufwendiger.

• Für den Fall rekursiver Funktionen sind die Bezeichnerumgebungen komplexer.

Bei jeder parametrisierten Softwarekomponente muss man sich überlegen und dokumentieren, - welche aktuellen Parameter bei einer Anwendung

zulässig sein sollen (der Anwender hat dann die Verantwortung, dass die Komponente nie mit unzulässigen Parametern angewendet wird);

üblicherweise sollte die Komponente für zulässige Parameter normal terminieren;

- bei welchen Parametern möglicherweise Ausnahmen ausgelöst werden und welche.

Zulässige Parameterwerte und Ausnahmen

Bemerkungen:

• Beachte: Es ist eine Entwurfsentscheidung, welche Parameter zulässig und welche unzulässig sind, da man in der Softwarekomponente die Parameter zunächst prüfen kann.

Beispiel: zwei Varianten einer Funktion foo:

1. fun foo (m,n) =

if m<n then m div n else foo (m-n, n) Zulässig für a) m < n, n ungleich 0

b) n > 0

2. exception unerwuenschteParameter;

fun foo (m,n) =

if ( m>= n orelse n=0 ) andalso n<=0 then raise unerwuenschteParameter else if m<n then m div n else foo (m-n, n) Bei dieser Variante könnte man alle Parameter als zulässig erklären.

Das Abprüfen der Zulässigkeit von Parametern (defensive Programmierung) führt zu besserer Stabilität, allerdings oft auf Kosten der Lesbarkeit und Effizienz der Softwarekomponente.

• Design by Contract:

Die Schnittstelle ist wie ein Vertrag zwischen - dem Anwender der Komponente (client), - demjenigen, der die Komponente realisiert

(provider).

Beispiel: Vertrag zur Funktion heapifiy

heapify : fvbintree -> int -> fvbintree Vorbedingung, die ein Anwender von

heapify b ix erfüllen sollte:

- ix muss eine Index von b sein, d.h. 0<= ix < size b - die Kinder von ix in b erfüllen die Heap-Eigenschaft Nachbedingung, die das Ergebnisevon

heapify b ix erfüllen sollte:

- size e = size b ;

- die Markierungen der Knoten, die sich nicht im Unterbaum von ix befinden, sind in e und b gleich;

- die Menge der Markierungen der Knoten, die sich im Unterbaum von ix befinden, sind in e und b gleich;

- der Knoten mit Index ix und alle seine Kinder in b erfüllen die Heap-Eigenschaft.

(6)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 246

3.4.2 Testen und Verifikation

Qualitätssicherung: Kleine Einführung

Bei der Qualitätssicherung in der Softwareentwicklung spielen zwei Fragen eine zentrale Rolle:

• Wird das richtige System entwickelt?

• Wird das System richtig entwickelt?

Validation hat die Beantwortung der ersten Frage zum Ziel. Beispielsweise ist zu klären, ob

- die benutzten Anforderungen die Vorstellungen des Auftraggebers richtig wiedergeben,

- die Anforderungen von allen Beteiligten gleich interpretiert werden,

- Unterspezifizierte Aspekte richtig konkretisiert wurden.

Ein zentraler Teil der Software-Entwicklung besteht darin zu prüfen, ob die entwickelte Software auch den gestellten Anforderungen entspricht.

Überblick:

- einführende Bemerkungen zur Qualitätssicherung - Testen

- Verifikation von Programmeigenschaften

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 247

Validation prüft also die Übereinstimmung von Software mit den Vorstellungen der Auftraggeber/

Benutzer bzw. mit der Systemumgebung, in der die Software eingesetzt wird.

Unter Verifikation verstehen wir den Nachweis, dass Software bestimmte, explizit beschriebene Eigenschaften besitzt. Beispiele:

- Mit Testen kann man prüfen, ob ein Programm zu gegebenen Eingaben die erwarteten Ausgaben liefert (Beschreibung: Testfälle).

- Mittels mathematischen Beweisen kann man zeigen, dass ein Programm für alle Eingaben ein bestimmtes Verhalten besitzt (Beschreibung:

boolesche Ausdrücke, logische Formeln).

- Nachweis, dass ein Programm einen gegebenen Entwurf und die darin festgelegten Eigenschaften besitzt (Entwurfsbeschreibung).

Liegen die beschriebenen Eigenschaften in einer formalen Sprache vor, kann die Verifikation automatisiert werden.

Zu prüfende Eigenschaften:

- Terminierung für zulässige Parameter - Verhalten wie im Entwurf festgelegt

• Grundsätzlich können sich Validation und Verifikation auf alle Phasen der Software- entwicklung beziehen.

• Wir betrachten im Folgenden Testen und Verifikation durch Beweis anhand einfacher Beispiele im

Kontext der funktionalen Programmierung. Eine systematischere Betrachtung von Aspekten der Qualitätssicherung ist Gegenstand von ESSy II.

Bemerkung:

Testen

Testen bedeutet die Ausführung eines Programms oder Programmteils mit bestimmten Eingabedaten.

Testen kann sowohl zur Validation als auch zur Verifikation dienen.

Bei funktionalen Programmen bezieht sich Testen überwiegend auf Eingabe- und Ausgabeverhalten von Funktionen. Wir betrachten:

• Testen mit Testfällen

• Testen durch dynamisches Prüfen

Testen mit Testfällen:

• Beschreibe das „Soll-Verhalten“ der Software durch eine (endliche) Menge von Eingabe-Ausgabe-Paaren.

• Prüfe, ob die Software zu den Eingaben der Testfälle die entsprechenden Ausgaben liefert.

Beispiel:

Testen der folgenden Funktionsdeklaration:

exception unzulaessigerParameter fun fac 0 = 1

| fac n = if 0<n orelse n<=12 then n * fac (n-1)

else raise unzulaessigerParameter Testfälle:

0 1

12 479001600

13 Ausnahme: unzulaessiger Parameter

~1 Ausnahme: unzulaessiger Parameter

~1073741824 Ausnahme: unzulaessiger Parameter

(7)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 250

Beobachtetes Verhalten:

0 1

12 479001600

13 Ausnahme: overflow

~1 keine Terminierung beobachtet

~1073741824 Ausnahme: overflow

• Das Verhalten von Funktionen mit unendlichem Argumentbereich kann durch Testen nur teilweise verifiziert werden. Testen kann im Allg. nicht die Abwesenheit von Fehlern zeigen.

• Wichtig ist die Auswahl der Testfälle. Sie sollten die „relevanten“ Argumentbereiche abdecken.

Bemerkung:

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 251

Testen durch dynamisches Prüfen:

• Beschreibe Eigenschaften von Zwischen- oder Ergebniswerten mit den Mitteln der Programmier- sprache (meist boolsche Ausdrücke); d.h.

implementiere Prüfprädikate.

• Rufe die Prüfprädikate an den dafür vorgesehenen Stellen im Programm auf.

• Lasse die Prüfprädikate in der Testphase des zu testenden Programms auswerten. Bei negativem Prüfergebnis muss eine Fehlermeldung erstellt werden.

Anders als beim Testen mit Testfällen wird also das Verhalten des Programms an bestimmten Stellen automatisch während der Auswertung geprüft.

1. Prüfung der Zulässigkeit von Parametern bei Aufruf 2. Prüfung durch Ergebniskontrolle

Beispiel: (typische Prüfungen)

Bemerkung:

Viele moderne Programmiersprachen bieten spezielle Sprachkonstrukte für das Testen durch dynamische Prüfung an.

Verifikation durch Beweis

Verifikation im engeren Sinne meint meist Verifikation durch Beweis. Hier verwenden wir den Begriff in diesem engeren Sinne.

Im Gegensatz zum Testen erlaubt Verifikation (durch Beweis) die Korrektheit zu zeigen, d.h. insbesondere die Abwesenheit von Fehlern.

Wir betrachten hier nur Programmverifikation, d.h. den Nachweis, dass ein Programm eine spezifizierte Eigenschaft besitzt.

Die Spezifikation kommt üblicherweise aus dem Entwurf bzw. den Anforderungen.

Zwei zentrale Eigenschaften:

- Programm liefert die richtigen Ergebnisse, wenn es terminiert (partielle Korrektheit).

- Programm terminiert für die zulässigen Eingaben.

Beide Eigenschaften zusammen ergeben totale Korrektheit.

Eine Funktionsdeklaration ggt zur Implementierung des ggT zweier natürlicher Zahlen soll implementiert werden.

Spezifikation:

Für m,n≥0, m,n nicht beide null soll gelten:

ggt(m,n) = max { k | k teilt m und n } Implementierung (Euklidscher Algorithmus):

(* m, n >= 0, nicht beide gleich 0 *) fun ggt (m,n) =

if m=0 then n else ggt( n mod m, m )

Beispiel: (Spezifikation)

Bei funktionalen Programmen spielen zwei Beweistechniken eine zentrale Rolle:

1. Strukturelle oder Parameterinduktion

2. Berechnungsinduktion (computational induction) Wir stellen nur die strukturelle Induktion vor.

(8)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 254

Parameterinduktion:

Bei der Parameterinduktion werden die Eigenschaften einer Funktion für alle Parameter gezeigt, indem man eine Induktion über die Menge der zulässigen

Parameter führt.

Beispiel: (Korrektheit von ggt)

Vorüberlegung:

Für n≥0, m>0 gilt:

k teilt m und n k teilt m und k teilt (n mod m)

Induktion über den Parameterbereich:

Wir zeigen:

a) ggt ist korrekt für m=0 und beliebiges n.

b) Vorausgesetzt ggt ist korrekt für alle (k,n) mit k≤m, dann auch für (m+1,n).

Ad a) Induktionsanfang:

ggt(0,n) = n = max { k | k teilt n }

= max { k | k teilt 0 und n }

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 255

Ad b) Induktionsschritt:

Voraussetzung: Sei m gegeben.

Für alle n, k mit k≤m gilt: ggt ist korrekt für (k,n) Zeige: Für alle n gilt: ggt ist korrekt für (m+1,n) ! ggt(m+1,n)

= (* Deklaration von ggt *) ggt(n mod (m+1),m+1)

= (* n mod (m+1)≤m, Induktionsvoraussetzung *) max { k | k teilt (n mod (m+1)) und m+1 }

= (* Vorüberlegung *) max { k | k teilt m+1 und n } QED.

Bemerkung:

So wie Testen Testfälle oder Prüfprädikate voraussetzt, so benötigt Verifikation mit Beweis eine Spezifikation oder andere Beschreibung der zu zeigenden

Eigenschaften.

Äquivalente Funktionsdeklarationen:

Wir haben gesehen, dass unterschiedliche Funktionsdeklarationen das gleiche Ein- und Ausgabeverhalten haben können.

Zum Beispiel kann eine Deklaration in einer

„aufwendigeren“ Rekursionsformen einfacher zu lesen sein, aber eine entsprechende lineare oder repetitive Funktionsdeklaration performanter sein.

Transformiert man die eine in die andere Form ist es wichtig, die Äquivalenz zu beweisen.

Bemerkung:

• Semantisch äquivalente Programme können große Unterschiede bei der Effizienz aufweisen.

• Bedeutungserhaltende Transformationen spielen in der Programmoptimierung und dem Refactoring von Software eine wichtige Rolle.

Wir betten die Fakultätsfunktion fun fac (n :int) :int =

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

in eine Funktion mit einem weiteren Argument ein, das Zwischenergebnisse aufsammelt:

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

else facrep( n-1, res*n ) Damit läßt sich die Fakultät definieren als:

fun fac1 (n :int) :int = facrep ( n, 1 ) ; Dadurch wurde eine lineare Rekursion in eine repetitive Form gebracht.

Korrektheit von Transformationen:

Wir transformieren die rekursive Funktionsdeklarationen in einfachere Deklarationen und zeigen Äquivalenz:

Beispiel: (linear repetitiv)

(9)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 258

Lemma:

Für die obigen Deklarationen von fac und facrep gilt:

∀n mit n≥0, r mit r≥0 : fac ( n ) * r = facrep ( n, r ) insbesondere: ∀n mit n≥0 : fac (n) = fac1 (n)

Beweis:

mittels Parameterinduktion nach n Induktionsanfang:

Zu zeigen: ∀r mit r≥0 : fac(0) * r = facrep( 0,r ) fac( 0 ) * r

= (* Deklaration von fac *)

( if 0 = 0 then 1 else 0 * fac (0-1) ) * r

= (* Ausdrucksauswertung *)

r

= (* Ausdrucksauswertung *)

if 0=0 then r else facrep( 0-1, r*0 )

= (* Deklaration von facrep *)

facrep( 0, r )

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 259

Induktionsschritt: k k+1 Induktionsvoraussetzung:

für k≥0 : ∀r mit r≥0 : fac ( k ) * r = facrep ( k, r ) Zu zeigen: fac ( k+1 ) * r = facrep ( k+1, r )

fac( k+1 ) * r

= (* Deklaration von fac *)

( if k+1 = 0 then 1 else (k+1) * fac (k+1-1) ) * r

= (* Ausdrucksauswertung *)

( (k+1) * fac (k) ) * r

= (* Kommutativität & Assoziativität der Multiplikation *) fac (k) * (r * (k+1))

= (* Induktionsvoraussetzung *)

facrep (k, r * (k+1) )

= (* Ausdrucksauswertung *)

if k+1=0 then r else facrep( k, r * (k+1) )

= (* Deklaration von facrep *)

facrep( k+1, r )

Wir wollen die Fibonacci-Funktion fib in eine lineare Form transformieren.

Idee: Führe zwei zusätzliche Parameter ein, die benutzt werden, um die Anzahl der Paare ausgehend vom Anfang zu berechnen.

Es soll also gelten:

fib1 (n) = fibemb( n,1,1) Wir definieren fibemb zu:

fun fibemb ( n: int, letzt: int, res: int ): int = if n<=1 then res

else fibemb( n-1, res, letzt+res ) Dadurch wurde eine kaskadenartige Rekursion in eine repetitive Form gebracht.

Beispiel: (kaskadenartig linear)

Beweis:

(siehe Vorlesung)

mittels Parameterinduktion nach n

Terminierung

Zentrale Eigenschaft einer Funktionsdeklaration ist, dass ihre Anwendung auf die zulässigen Parameter terminiert.

Diese Eigenschaft gilt für alle nicht-rekursiven

Funktionsdeklarationen, die sich nur auf terminierende Funktionen abstützen.

Bei rekursiven Funktionsdeklarationen muss die Terminierung nachgewiesen werden.

Idee: Die Parameter sollten bei jedem rekursiven Aufruf „kleiner“ werden.

Beispiele: („kleiner werdende“ Parameter)

1. fun mapplus [ ] = 0

| mapplus (x::xs) = x + mapplus xs 2. fun einfuegen ( [ ], el, ix ) = [ el ]

| einfuegen ( xs, el, 0 ) = el::xs

| einfuegen ( x::xs, el, ix ) = einfuegen (xs,el, ix -1) 3. fun foo (m,n) =

if m<n then m div n else foo (m-n, n)

(10)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 262

Definition: (Ordnung)

Eine Teilmenge R von M x N heißt eine (binäre) Relation. Gilt M=N, dann nennt man R homogen.

Eine homogene Relation heißt:

• reflexiv, wenn für alle x∈M gilt: (x,x)∈R

• antisymmetrisch, wenn für alle x, y∈M gilt:

wenn (x,y)∈R und (y,x)∈R, dann x = y

• transitiv, wenn für alle x, y, z∈M gilt:

wenn (x,y)∈R und (y,z)∈R, dann (x,z)∈R Eine reflexive, antisymmetrische und transitive homogene Relation auf M x M heißt eine (partielle) Ordnungsrelation.

Eine Menge M mit einer Ordnungsrelation R heißt eine (partielle) Ordnung.

Meist benutzt man Infixoperatoren wie ≤ (oder ⊆) zur Darstellung der Relation und schreibt

x≤y statt (x,y)∈R .

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 263

Definition: (Kette, noethersche Ordnung)

Sei (M,≤) eine Ordnung. Eine Folgeϕ: IN M heißt eine (abzählbar unendliche) aufsteigende Kette, wenn für alle i∈IN gilt:

ϕ(i) ≤ ϕ(i+1)

(absteigende Kette: entsprechend).

Eine Ketteϕwird stationär, falls es ein j∈IN gibt, so dass

ϕ(j) =ϕ(j+k) für alle k∈IN.

Sei N eine Teilmenge von M. x∈N heißt : größtes Element von N, wenn∀y∈N gilt: y≤x . kleinstes Element von N, wenn∀y∈N gilt: x≤y . maximales Element von N, wenn∀y∈N gilt:

x≤y impliziert x=y . minmales Element von N, wenn∀y∈N gilt:

y≤x impliziert x=y .

Eine Ordnung (M,≤) heißt noethersch, wenn jede nicht-leere Teilmenge von M ein minimales Element besitzt.

Lemma:

Eine Ordnung ist genau dann noethersch, wenn jede absteigende Kette stationär wird.

Beweis:

(siehe Theorievorlesung)

Terminierungskriterium:

Sei f: ST eine rekursive Funktionsdeklaration mit formalem Parameter n und sei P die Menge der zulässigen Parameter von f.

Jede Anwendung von f auf Elemente von P terminiert, - wenn es eine noethersche Ordnung (M,≤) und - eine Abb. δ: P M gibt,

- so dass für jede rekursive Anwendung f (G(n) ) im Rumpf der Deklaration gilt:

i) G(n) ist ein zulässiger Parameter, d.h. G(n)∈P.

ii) Die aktuellen Parameter werden echt kleiner, d.h.

δ(G(n)) <δ(n) .

Bemerkung:

• Da die aktuellen Parameter nur endlich oft echt kleiner werden können (und dann stationär werden), garantiert das obige Kriterium die Terminierung.

• Um die Terminierung nachzuweisen, muss man also eine geeignete noethersche Ordnung und eine geeignete Abbildungδfinden.

• Ist der Argumentbereich bereits noethersch geordnet, kannδselbstverständlich auch die Identität sein.

Beispiele: (Terminierungsbeweis)

1. fun mapplus xs =

if null xs then 0 else (hd xs) + mapplus ( tl xs ) P ist die Menge aller endlichen Listen.

Noethersche Ordnung (IN,≤).

Alsδwähle die Funktion länge (Länge einer Liste).

Zu zeigen:

i) tl xs ist ein zulässiger Parameter : ok.

ii) länge( tl xs) < länge( xs ) : ok.

(11)

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 266

2. fun einfuegen ( [ ], el, ix ) = [ el ]

| einfuegen ( xs, el, 0 ) = el::xs

| einfuegen ( x::xs, el, ix ) = einfuegen (xs,el, ix -1) P ist die Menge aller Tripel aus ‘a list * ‘a * int . Noethersche Ordnung (IN,≤).

Alsδwähle die Funktion längefst, die länge auf die erste Komponente anwendet.

Zu zeigen:

i) (xs, el, ix-1 ) ist ein zulässiger Parameter : ok.

ii) längefst( xs, el, ix-1 ) < längefst( x::xs,el,ix ) : ok.

Bemerkung:

Hätte man stattdessen fürδdie Selektion auf die dritte Komponente gewählt, hätte man Terminierung nur für eine kleinere Menge zulässiger Parameter zeigen können, nämlich z.B. für Parametertripel (xl,el,ix) mit ix≥0.

20.11.2006 © A. Poetzsch-Heffter, TU Kaiserslautern 267

3. fun foo (m,n) =

if m<n then m div n else foo (m-n, n) P ist die Menge aller Paare (m,n) aus int * int

mit (m < n und n≠0) oder n > 0 Noethersche Ordnung (IN,≤).

δ: Z x Z IN mit

0 , falls m < n δ( m,n ) =

m-n+1, falls m≥n Zu zeigen:

i) (m-n,n) ist ein zulässiger Parameter : ok.

ii) Unter der Voraussetzung m≥n und n > 0 : 1. Fall: m-n≥n :

δ(m-n,n) = m-n-n+1 < m-n+1 = δ(m,n) 2. Fall: m-n < n :

δ(m-n,n) = 0 < m-n+1 = δ(m,n)

Bemerkung:

• Terminierungsbeweise sind bei der Entwicklung von Qualitätssoftware sehr wichtig, und zwar unabhängig vom verwendeten Modellierungs- bzw. Programmierparadigma.

• Es sollte zur Routine der Softwareentwicklung, gehören, den zulässigen Parameterbereich festzulegen und dafür Terminierung zu zeigen.

Referenzen

ÄHNLICHE DOKUMENTE

Schritt: Milchtest nach ausgewählten Kriterien und Präsentation Kompetenzen und Unterrichtsinhalte: • Die Schüler werden in ihrer Rolle als mündige Verbraucher gestärkt und

Dennoch: Der Staat ist nicht über- schuldet, die Sozialausgaben sind nicht auf Pump finanziert, auch wer- den von der Frente weder Presse und Institutionen noch die freie Wirt-

Und weshalb erwähnt man, wenn es um Armutsbekämpfung geht, lieber groß- sprecherische Potentaten wie Hugo Chávez oder die Castro-Brüder und nicht Costa Rica, das

Informationen über die Verarbeitung personenbezogener Daten in der Sicherheits- und Ordnungs- verwaltung der Stadtverwaltung Gotha und über Ihre Rechte nach der

Oder ob nicht doch durch die Nominierung von Schäuble eine Große Koalition vorzuziehen wäre oder gar eine Ampel-Konstella- tion als demokratische Mehrheitsalternative.. Man

Die Auswertung eines Ausdrucks A kann in ML - zu einem Ergebnis führen, dem Wert von A, - eine Ausnahme auslösen,.. -

I Das Prinzip ist die Abstraktion vom Programmzustand durch eine logische Sprache; insbesondere wird die Zuweisung durch Substitution modelliert.. I Der Trick behandelt

38 Körperteile 0 kann kein Körperteil zeichnen und benennen Basaler Bereich benennen und ein- 1 kann zwei Körperteile zeichnen und benennen (Körperwahrnehmung) zeichnen 2 kann drei