Verifikation von funktionalen Programmen Funktionale Programmiersprachen sind mathematisch orientiert. Auf dieser Basis können Programme mit wohlbekannten mathematischen Methoden betrachtet werden. In diesem Zusammenhang existieren Möglichkeiten zur • Verifikation von Programmeigenschaften (mithilfe von Induktionsprinzipien für rekursive Funktionen und Datentypen) sowie zur • Herleitung optimierter rekursiver Funktionsdefinitionen (Unfold/Fold-Verfahren, Bird/Meertens-Formalismus). Für die Menge der natürlichen Zahlen ` gelten die Beweisprinzipien der vollständigen Induktion, mit denen sich Eigenschaften für alle natürlichen Zahlen nachweisen lassen. Mit dem Induktionsprinzip kann man insbesondere die Korrektheit rekursiver Funktionsdefinitionen zeigen. Das Induktionsprinzip selbst basiert dabei auf der induktiven Definition der natürlichen Zahlen. Induktionsbeweise lassen sich auch auf anderen induktiv definierten Strukturen führen. Ein Induktionsprinzip allgemein für Datentypen ist die strukturelle Induktion. Mithilfe einer weiteren Verallgemeinerung, der wohlfundierten Induktion, können Beweise für beliebige Strukturen, auf denen eine Relation (mit bestimmter Eigenschaft) definiert ist, geführt werden. Definition: Natürliche Zahlen Aus der Definition resultiert das Beweisprinzip der vollständigen Induktion. Beweisprinzip: Vollständige Induktion Zum Beweis einer Eigenschaft P, die für alle n ∈ ` gelten soll (∀n ∈ ` .P(n)), reicht es zu zeigen: 1. Induktionsanfang: Es gilt P(0) 2. Induktionsschluß: Für alle n ∈ ` folgt aus P(n) die Gültigkeit von P(succ(n)). Eine implizite Voraussetzung ist dabei, daß P für alle n ∈ ` definiert ist. Definition: Menge der endlichen Listen Analog zum Prinzip der vollständigen Induktion für natürliche Zahlen wird das Prinzip der Listeninduktion zum Beweis von Aussagen über endliche Listen betrachtet. Die Menge aller endlichen Listen über einem Grundtyp a wird wie folgt definiert: Beweisprinzip: Listeninduktion Sei nun P eine Eigenschaft von Listen, die unabhängig von den einzelnen Listenelementen und für alle Listen definiert ist. Dann gilt P für alle endlichen Listen, falls 1. Induktionsanfang: P([]) gilt 2. Induktionsschluß: Für alle Elemente x :: a und ale Listen xs :: [a] gilt P(xs) impliziert P(x : xs). Beispiel: reverse: 1. Version reverse :: [a] -> [a] reverse [] = [] reverse (x:xs) = reverse xs ++ [x] 1 reverse: 2. Version rev :: [a] -> [a] rev xs = aux xs [] aux :: [a] -> [a] -> [a] aux [] ys = ys aux (x : xs) ys = aux xs (x : ys) Sind beide Versionen gleichwertig, d.h. gilt für alle endlichen Listen liste: reverse liste = rev liste? Lemma 1: Assoziativität der Listenverkettung (++) :: [a] -> [a] -> [a] [] ++ ys = ys (x : xs) ++ ys = x : (xs ++ ys) Die Funktion (++) ist assozativ, d.h. für beliebige endliche Listen xs, ys, zs :: [a] gilt: xs ++ (ys ++ zs) = (xs ++ ys) ++ zs. Beweis: Lemma 1 Induktionsanfang: xs = [] [] ++ (ys ++ zs) = ys ++ zs = ([] ++ ys) ++ zs Induktionsschluß: xs = x : xs’ (x : xs’) ++ (ys ++ zs) = x : (xs’ ++ (ys ++ zs)) = x : ((xs’ ++ ys) ++ zs) = (x : (xs’ ++ ys)) ++ zs = ((x : xs’) ++ ys) ++ zs Lemma 2: reverse ist zu sich selbst invers Die Funktion reverse ist zu sich selbst invers, d.h. für jede endliche Liste xs :: [a] gilt: reverse (reverse xs) = xs Dabei sei reverse nach 1. Version definiert! Beweis: Lemma 2 Induktionsanfang: xs = [] reverse (reverse [] ) = reverse [] = [] Induktionsschluß: xs = x : xs’ reverse (reverse (x : xs’)) = reverse (reverse xs’ ++ [x]) ? Zusammenhang Listenkonkatenation und Listenspiegelung Für alle y :: a und ys :: [a] gilt: reverse (ys ++ [y]) = (y : reverse ys) Beweis : Induktionsanfang: ys = [] reverse ([] ++ [y]) = reverse [y] = reverse [] ++ [y] = [] ++ [y] = [y] = y : reverse [] Induktionsschluß: ys = z : zs reverse ((z : zs) ++ [y]) = reverse (z : (zs ++ [y])) = reverse (zs ++ [y]) ++ [z] 2 = (y : reverse zs) ++ [z] = y : (reverse zs ++ [z]) = y : reverse (z : zs) Einsatz im Beweis für Lemma 2: reverse (reverse (x : xs’)) = reverse (reverse xs’ ++ [x]) = x : reverse (reverse xs’) = x : xs’ Lemma 3: Für alle endlichen Listen xs :: [a] gilt: reverse xs = rev xs Zunächst gilt für alle xs, ys :: [a]: aux xs ys = reverse xs ++ ys Beweis Lemma 3: Induktionsanfang: xs = [] aux [] ys = ys = [] ++ ys = reverse [] ++ ys Induktionsschluß: xs = x : xs’ aux (x : xs’) ys = aux xs’ (x : ys) = reverse xs’ ++ (x : ys) = reverse xs’ ++ ([x] ++ ys) = (reverse xs’ ++ [x]) ++ ys = reverse (x : xs’) ++ ys Daraus folgt : rev xs = aux xs [] = reverse xs ++ [] = reverse xs Definition: Strukturelle Induktion Übertragung des Prinzips der Listeninduktion auf andere algebraische Datenstrukturen. Allgemein spricht man dann von struktureller Induktion. Beweisprinzip: Strukturelle Induktion Beispiel: Der Datentyp Tree a definiere einen Binärbaum data Tree a = Leaf a | Node (Tree a) (Tree a). Induktionsprinzip: 1. Zunächst wird gezeigt, daß die Eigenschaft für Bäume der Form Leaf a gilt. 2. Danach wird gezeigt, daß aus der Gültigkeit für die Bäume l und r die Gültigkeit der Eigenschaft für den Baum Node l r folgt. Annahme: für l und r gilt die Eigenschaft. Nachweis von Programmeigenschaften: Insbesondere über Listen und anderen strukturierten Datenobjekten sind Funktionen höherer Ordnung zur Definition allgemeiner Schemata der Listenverarbeitung einsetzbar. 3 In der Listenverarbeitung unterscheidet man drei Grundtypen: • Anwendung einer Transformation auf alle Listenelemente, z.B. alle Listenelemente verdoppeln, inkrementieren, quadrieren, codieren. • Faltung von Listen, z.B. Aufaddieren, Maximumsbestimmung. • Filtern von Listen. filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p = (x : xs) if (p x) then x : filter p xs else filter p xs oder filter p xs = [x | x <- xs, p x] Analog zu den Beweisen von Eigenschaften von Funktionen erster Ordnung erfolgen Beweise für Funktionen höherer Ordnung. Eigenschaften wie die folgenden sind insbesondere im Kontext von Programmtransformationen hilfreich: 1. map (f.g) = (map f) . (map g) 2. filter p . map f = (map f . filter (p . f)) 3. foldr f e (xs ++ ys) = f (foldr f e xs) (foldr f e ys) Beispiel: Beweis höherer Funktionen für beliebige Listenargumente Induktionsanfang: (filter p . map f) [] = filter p (map f [])´ = filter p [] = [] (map f . filter (p . f)) [] = map f (filter (p . f) [] ) = map f [] = [] Induktionsschluß: (filter p . map f) (x : xs’) = filter p (map f (x : xs’)) = filter p ((f x) : map f xs’) Fallunterscheidung: p (f x) hat den Wert True = filter p ((f x) : map f xs’) = (f x) : filter p (map f xs’) = (f x) : (map f (filter (p . f) xs’)) = map f (x : (filter (p . f) xs’)) = (map f . filter (p . f)) (x : xs’) Für den Fall, daß p (f x) den Wert False hat, erfolgt der Nachweis analog. Falls p (f x) nicht definiert sein sollte, gilt die Gleichheit, denn in diesem Fall wären beide Ausdrücke auch nicht definiert. 4 Definition: Wohlfundierte Induktion Alle zuvor beschriebenen Induktionsprinzipien haben Ordnungseigenschaften ihrer zugrundeliegenden Bereiche ausgenutzt. Bei den natürlichen Zahlen war das die < Relation (für P wird im Induktionsschritt von der Gültigkeit von n auf die Gültigkeit für n+1 geschlossenen, wobei n < n+1 gilt). Bei der strukturellen Induktion wurde von einfacheren Termen auf komplexere Terme geschlossen (eine entsprechende Ordnung wird über die Größe der Terme definiert). Bei den meisten Induktionsprinzipien handelt es sich um eine Instanz des sehr allgemeinen Prinzips der wohlfundierten Induktion. Das Prinzip bezieht sich in seiner Basis auf das Konzept der wohlfundierten Relation. Prinzip der strukturellen Induktion für Listen: Um zu beweisen, daß die logische Eigenschaft P(xs) gilt für jede beliebige endliche Liste xs, müssen zwei Dinge gezeigt werden: Basisfall: Zeige P([]) direkt. Induktionsschritt: Zeige P(x:xs) durch Verwenden der Annahme, daß P(xs) gilt. Mit anderen Worten P(xs) ⇒ P(x:xs) muß gezeigt werden. P(xs) wird als die Induktionsannahme bezeichnet, da es als richtig angenommen wird, um P(x:xs) zu zeigen. Beispiel: sum :: [Int] -> Int sum [] = 0 sum (x:xs) = x + sum xs Für [] wird direkt ein Wert angegeben, und der Wert für sum(x.xs) wird definiert durch den wert sum xs. Beispiel: doubleAll [] = [] doubleAll (z:zs) = 2*z : doubleAll zs 5 Aufgabe: Zu zeigen: sum(doubleAll xs) = 2 * sum xs für alle endlichen Listen xs. Lösung: Induktionsbasis: sum(doubleAll []) = 2 * sum [] Induktionsschritt: sum(doubleAll (x:xs)) = 2 * sum (x:xs) Zu zeigen unter Verwendung der Induktionsannahme: sum(doubleAll xs) = 2 * sum xs Induktionsbasis: linke Seite: sum(doubleAll []) = sum [] =0 rechte Seite: 2 * sum [] =2*0 =0 Beide Seiten sind gleich, also gilt die Induktionsbasis. Induktionsschritt: linke Seite: sum (doubleAll (x:xs)) = sum (2*x : doubleAll xs) = 2*x + sum (doubleAll xs) rechte Seite: 2*sum(x:xs) = 2* (x + sum xs) = 2*x + 2*sum xs Weitere Vereinfachung der linken Seite unter Verwendung der Induktionsannahme: sum (doubleAll (x:xs)) = sum (2*x : doubleAll xs) = 2*x + sum (doubleAll xs) = 2*x + 2 * sum xs Beide Seiten stimmen überein. Beweis vollständig. 6 Aufgabe: Zu zeigen: length (xs ++ ys) = length xs + length ys Definition: length length [] = 0 length (z:zs) = 1 + length zs Definition: ++ [] ++ zs = zs (w:ws) ++ zs = w : (ws++zs) Lösung: Für den Basisfall muß gezeigt werden: length ([] ++ ys) = length [] + length ys Und für den Induktionsschritt muß gezeigt werden: length ((x:xs) ++ ys) = length (x:xs) + length ys mittels der Induktionsannahme: length(xs ++ ys) = length xs + length ys Induktionsbasis: linke Seite: length ([] ++ ys) = length ys rechte Seite: length [] + length ys = 0 + length ys = length ys Induktionsschritt: linke Seite: length ((x:xs) ++ ys) = length (x:(xs ++ ys)) = 1 + length (xs ++ ys) Unter Verwendung der Induktionsannahme: = 1 + length xs + length ys rechte Seite: length (x:xs) + length ys = 1 + length xs + length ys Beide Seiten stimmen überein. Beweis vollständig. 7 Aufgabe: Zu zeigen: reverse (xs ++ ys) = reverse ys ++ reverse xs Definition reverse: reverse [] = [] reverse (z:zs) = reverse zs ++ [z] Lösung: Induktionsbasis: reverse ([] ++ ys) = reverse ys ++ reverse [] Induktionsschritt: reverse ((x:xs) ++ ys) = reverse ys ++ reverse (x:xs) Induktionsannahme: reverse (xs ++ ys) = reverse ys ++ reverse xs Induktionsbasis: linke Seite: reverse ([] ++ ys) = reverse ys rechte Seite: reverse ys ++ reverse [] = reverse ys ++ [] Wir können nur zeigen, daß die beiden Seiten gleich sind, wenn wir zeigen, daß das Anhängen einer leeren Liste an das Ende einer Liste eine Identitätsoperation ist. xs ++ [] = xs Zu zeigen mittels Induktion über xs. Induktionsschritt: linke Seite: reverse ((x:xs) ++ ys) = reverse (x:xs ++ ys)) = reverse (xs ++ ys) ++ [x] = (reverse ys ++ reverse xs) ++ [x] rechte Seite: reverse ys ++ reverse (x:xs) = reverse ys ++ (reverse xs ++ [x]) ++ ist assoziativ: xs ++ (ys ++ zs) = (xs ++ ys) ++ zs Beide Seiten stimmen überein. Beweis vollständig. 8 Aufgabe: Zeigen Sie die Identität qrev y (a++b) = qrev (qrev y a) b durch Induktion über a. Definition grev: qrev x [] = x qrev x (y : ys) = qrev (y : x) ys Lösung: Wir nummerieren die Gleichung in ++ und qrev: [] ++ b = b (app1) (a : as) ++ b = a : (as ++ b) (app2) qrev x [] = x qrev x (y : ys) = qrev (y:x) ys (qrev1) (qrev2) Basisfall: a = [] qrev y ([] ++ b) = qrev y b (app1) = qrev (qrev y []) b (qrev1) Sei nun a = a : as. Nach Induktionsvoraussetzung ist qrev y (a++b) = qrev (qrev y q) b Im Induktionsschritt ist qrev y ((a:as)++b) = qrev (qrev y (a:as)) b zu zeigen. qrev y ((a:as)++b) = qrev y a:(as++b) (app2) = qrev a:y as++b (qrev2) = qrev (qrev a:y as) b (Ind.Hyp.) = qrev (qrev y (a:as))b (qrev2) Betrachten Sie die Gleichungen qrev x [] = x [] ++ x = (qrev [] []) ++ x qrev x (y:ys) = qrev (y:x) ys = (qrev [] ys) ++ (y :x) = (qrev [] ys) ++ ([y] ++ x) = (qrev [y] ys) ++ x = (qrev [] (y :ys)) ++ x (1) (2) (3) (4) (5) (6) Handelt es sich um einen Beweis? Falls ja: was wird beweisen? Ergänzen Sie gegebenenfalls den Beweis und kommentieren Sie ihn. Es handelt sich um einen Induktionsbeweis der Gleichung qrev x y = qrev [] y ++ x Die Induktion erfolgt über die Liste y. Basisfall: qrev x [] = x (qrev1) = [] ++ x (app1) = (qrev [] []) ++ x (qrev1) Induktionsschritt: qrev x (y:ys) = qrev(y:x) ys (qrev2) = (qrev [] ys) ++ (y :x) (Ind.Hyp.) 9 = (qrev [] ys) ++ ([y] ++ x) = (qrev [y] ys) ++ x = (qrev [] (y :ys)) ++ x (*) (Assoc. ++), (Ind. Hyp.) (qrev2) Assoziativität von ++ wurde in der Vorlesung bewiesen. Beweis von (*) mit (app2): [y] ++ x = (y:[]) ++ x = (y : ([] ++ x) = y : x 10