TU Kaiserslautern Prof. Dr. A. Poetzsch-Heffter Dipl.-Inf. J.-M. Gaillourdet Fachbereich Informatik Dipl.-Inf. P. Michel AG Softwaretechnik Übungsblatt 8: Software-Entwicklung 1 (WS 2009/10) Ausgabe: in der Woche vom 14.12. bis zum 18.12. Abgabe: in der Woche vom 04.01. bis zum 08.01. Abnahme: max. zwei Tage nach der Übung Aufgabe 1 Parameterinduktion (Präsenzaufgabe) Geben Sie die Implementierung der Haskell-Funktion gauss an, welche für gegebenen Parameter n die Summe der natürlichen Zahlen von 0 bis n durch rekursive Berechnung ermittelt. Zeigen Sie für Ihre Implementierung unter Verwendung der vollständigen Induktion, dass die folgende Gleichheit gilt: gauss n = n(n+1) 2 Aufgabe 2 Parameterinduktion (Einreichaufgabe) a) Geben Sie die Implementierung der Haskell-Funktion multiply an, welche die Multiplikation zweier natürlicher Zahlen auf Addition / Subtraktion zurückführt. Zeigen Sie für Ihre Implementierung unter Verwendung der vollständigen Induktion, dass die folgende Gleichheit gilt: multiply x y = x·y b) Beweisen Sie, dass die Haskell-Funktion g :: Integer -> Integer g n = h n 1 0 h :: Integer -> Integer -> Integer -> Integer h 0 _ r = r h n l r = h (n -1) r (l+r) ebenfalls die Fibonacci-Zahlen f f f f :: Integer -> Integer 0 = 0 1 = 1 n = f (n - 1) + f (n - 2) berechnet, also g n = f n für alle n ∈ N gilt. Zeigen Sie hierzu zunächst mittels Induktion über k, dass h (n-k) (f (k-1)) (f k) = f n für n, k ∈ N mit 1 ≤ k ≤ n und n > 0 gilt. Nutzen Sie dieses Teilergebnis, um die eigentliche Aussage zu beweisen. Hinweis: Im Fall obiger Induktion über k ist es ratsam, bei k = n zu beginnen und den Induktionsschluß k → (k − 1) durchzuführen! Aufgabe 3 Terminierung (Präsenzaufgabe) In dieser Aufgabe wollen wir die Terminierung von Funktionen mit dem Verfahren aus der Vorlesung beweisen. Wir betrachten wieder einmal die Fibonacci Funktion, implementiert durch: f f f f :: Integer -> Integer 0 = 0 1 = 1 n = f (n - 1) + f (n - 2) Beweisen Sie, dass diese Funktion terminiert, denken Sie aber daran, dass wir uns nur für Parameter aus den natürlichen Zahlen interessieren. Aufgabe 4 Terminierung (Einreichaufgabe) a) Beweisen Sie, dass die Funktion divConstZero, die wir im Rahmen der Expressions-Aufgabe geschrieben haben, terminiert: data Expr = Const Integer | Binary Op Expr Expr deriving (Eq , Ord , Show) data Op = Mult | Div | Plus | Minus deriving (Eq , Ord , Show) divConstZero divConstZero divConstZero divConstZero :: Expr -> Bool (Const _) = False ( Binary Div _ (Const 0)) = True ( Binary _ a b) = divConstZero a || divConstZero b Hinweis: Betrachten Sie ausschließlich endliche Ausdrücke und machen Sie dies im Beweis klar. b) Wir betrachten folgende Implementierung der Primfaktorzerlegung (für Zahlen ≥ 2) und wollen beweisen, dass sie für jede Eingabe terminiert: primfaktoren :: Integer -> [ Integer ] primfaktoren c = if c < 2 then [c] else faktor (c, 2) faktor faktor if c if c :: (Integer , Integer ) -> [ Integer ] (c, x) = == x then [x] `mod` x /= 0 then faktor (c, x + 1) x : faktor (c `div` x, 2) else else Da die Funktion primfaktoren nicht rekursiv ist, terminiert sie, wenn alle von ihr aufgerufenen Funktionen terminieren. Wir müssen also beweisen, dass die Funktion faktor terminiert, zumindest auf dem Parameter-Bereich, der von primfaktoren oder faktor aufgerufen wird. Sei also P der zulässige Parameterbereich: P = (c, x) | 2 ≤ x ≤ c ⊂ N × N Beweisen Sie, dass es durch einen Aufruf von primfaktoren (und die Folgeaufrufe durch faktor selbst) nur zulässige Aufrufe von faktor mit einem Parameter aus P geben wird: G(n) ∈ P. c) Um die Terminierung nun zu beweisen, verwenden wir das Verfahren aus der Vorlesung. Als noethersche Ordnung verwenden wir die Menge der Paare aus natürlichen Zahlen (N × N, ≤) mit der komponentenweisen Ordnung, d.h. ist die erste Komponente gleich wird nach der zweiten geordnet, wie auf natürlichen Zahlen, ansonsten wird nach der ersten Komponente geordnet. Das kleinste Element ist dann einfach (0, 0). Geben Sie eine Funktion δ : P 7→ N × N an, die Parameter auf unsere Ordnung abbildet, so dass Sie den Terminierungsbeweis führen können. Im vorhergehenden Aufgabenteil haben Sie schon bewiesen, dass G(n) ∈ P, es bleibt also noch zu zeigen, dass die Parameter echt kleiner werden: δ(G(n)) < δ(n). Aufgabe 5 Java (Einreichaufgabe) Im weiteren Verlauf der Vorlesung wird die Sprache Java verwendet. In dieser Aufgabe soll ein kleines “Hallo Welt”-Programm geschrieben, übersetzt und ausgeführt werden. a) Legen Sie eine Datei mit dem Namen “Hallo.java” an und geben Sie folgendes Javaprogramm ein: public class Hallo { static String greeting = "Hallo Java"; public static void main ( String [] args) { IO. println ( greeting ); } } Beachten Sie, dass eine Java Quelltextdatei immer den gleichen Namen wie die in ihr enthaltene Klasse plus die Endung “.java” trägt. In unserem Fall heisst die Klasse Hallo und die Datei entsprechend Hallo.java. b) Im nächsten Schritt werden wir diese Datei übersetzen, d.h. Bytecode für die Java Virtuelle Maschine erzeugen. Dazu laden Sie als erstes die Datei “IO.java” von der Webseite der Vorlesung und speichern diese im gleichen Verzeichnis wie Ihren Quelltext (Quellverzeichnis). Rufen Sie im Quellverzeichnis den Javacompiler auf der Kommandozeile mit folgendem Befehl auf: > javac Hallo.java Dem Compiler wird die Datei übergeben, die eine main-Methode enthält. Er erstellt dann zu jeder benutzen Klasse eine “.class”-Datei, die sich im selben Verzeichnis befindet. In unserem Fall müssten Sie jetzt Hallo.class und IO.class in ihrem Verzeichnis haben. Um das gerade geschriebene und übersetzte Programm auszuführen, rufen Sie aus dem Quellverzeichnis java mit der Klasse auf, die eine main-Methode enthält. In unserem Fall also: > java Hallo Wenn jetzt der Begrüßungstext auf dem Bildschirm erscheint haben Sie alles richtig gemacht. c) Um den Bytecode von den Quelldateien sauber zu trennen, können wir angeben wohin der Übersetzer seine Ausgabe speichert. Legen Sie ein Unterverzeichnis “build” an, in dem wir nun alle übersetzten Dateien ablegen. Hinweis: Löschen Sie die “class” Dateien die Sie in b) im Quellverzeichnis erzeugt haben, um Probleme mit Java zu vermeiden! Rufen Sie im Quellverzeichnis den Javacompiler auf der Kommandozeile mit folgendem Befehl auf: > javac -d build Hallo.java Er erstellt nun im build-Verzeichnis zu jeder benutzen Klasse eine “.class”-Datei. Um das Programm auszuführen rufen Sie aus dem Quellverzeichnis java mit der zusätzlichen Angabe wo sich die Klassen befinden auf: > java -cp build Hallo Wenn jetzt der Begrüßungstext auf dem Bildschirm erscheint haben Sie alles richtig gemacht. Aufgabe 6 Imperative Programmierung mit Java Kreuzen Sie an, ob folgende Aussagen wahr oder falsch sind. Bereiten Sie diese Aufgabe bis zu Ihrer nächsten Übungsstunde vor, so dass Sie bei Unklarheiten nachfragen und die Antworten diskutieren können. wahr falsch Ausdrücke in Java erzeugen keine Seiteneffekte. Auf der rechten Seite einer Zuweisung kann ein Prozeduraufruf stehen. Der Rumpf einer while-Anweisung wird mindestens einmal ausgeführt. Geschachtelte Fallunterscheidungen lassen sich durch eine Auswahlanweisung ersetzen. Auf jede Variable kann während der gesamten Programmausführung zugegriffen werden. Eine Variable des Typs boolean[] speichert ein Feld. Jeder Ausdruck ist eine Anweisung. Es gibt Anweisungen, welche die Abfolge, in der Anweisungen abgearbeitet werden, beeinflussen. Überall dort wo eine einzelne Anweisung stehen kann, kann auch ein Anweisungsblock stehen. Die Schleifenvariable einer for-Schleife kann auch um mehr als eins pro Schleifendurchlauf erhöht werden. Prozeduren enthalten Anweisungen und liefern immer ein Ergebnis. Eine Prozedur kann sich in ihrem Rumpf selbst aufrufen. Jede Prozedurinkarnation hat ihre eigenen lokalen Variablen. Wenn v eine lokale Variable einer Prozedur ist, dann wird mit v immer dieselbe Speichervariable angesprochen. Ein Programm kann Felder erzeugen, auf die es später nicht mehr zugreifen kann. Es kann mehrere Variablen geben, die auf dasselbe Feld zeigen. Aufgabe 7 Weihnachtsaufgabe (freiwillige Zusatzaufgabe) Mit Hilfe von Turtlegraphics lassen sich auf einfache Art und Weise sehr komplexe Grafiken generieren. Man stelle sich dafür vor, dass man eine Schildkröte (engl. Turtle) über eine Fläche steuert, die dabei eine Spur hinterlässt. Die Schildkröte reagiert auf einfache Befehle, wie “vorwärts” oder “drehen”. Die Schildkröte startet im Ursprung (0, 0) des karthesischen Koordinatensystems und schaut nach rechts (Richtung x-Achse). Sie reagiert auf die folgenden Befehle: forward x left x / right x up / down Die Schildkröte läuft vorwärts in Blickrichtung und legt eine Strecke von x zurück. Die Schildkröte dreht sich um x Grad nach links/rechts. Die Schildkröte hört auf zu zeichnen / setzt wieder an zu zeichnen. Wie auch bei Ein-/Ausgabe in Haskell, so ist auch bei Schildkröten-Programmen die Reihenfolge der Befehle relevant! Analog zum Typ IO a verwenden wir also Turtle a Befehle, die in do-Blöcken kombiniert werden können. Betrachten wir den Effekt des folgenden Turtle-Programms: nico :: Turtle () nico = do left 90 forward 100 right 45 forward (sqrt 5000) right 90 forward (sqrt 5000) right 45 -- Bild 1 forward 100 right 135 forward (sqrt 20000) right 135 forward 100 -- Bild 2 right 135 forward (sqrt 20000) left 135 forward 100 -- Bild 3 Laden Sie sich die Grundbibliothek “Turtle.hs” sowie die Datei “PDFTurtle.hs” (oder eine andere TurtleImplementierung Ihrer Wahl) von der Vorlesungsseite. Wenn Sie nun das Modul PDFTurtle importieren, erhalten Sie sowohl alle Turtle-Befehle, als auch die Funktion render :: String -> Turtle a -> IO (). Diese nimmt einen Namen sowie ein Turtle-Programm und gibt das entstehende Bild im Sinne der Implementierung aus (hier: als PDF-Dokument). Da alle Implementierungen die gleiche Schnittstelle haben, können Sie jederzeit durch Austauschen des import auf eine andere Implementierung wechseln. a) Zeichnen Sie ein Dreieck, eine eckige Spirale und einen Kreis: Hinweis: Da die Schildkröte ausschließlich geradeaus laufen kann, müssen Sie den Kreis approximieren. Hinweis: Sie müssen sich nicht darum kümmern wie groß Ihre Grafik insgesamt wird. Einzig die relative Größe und Position zwischen Objekten einer Grafik ist für die Ausgabe relevant. b) Viele Grafiken lassen sich mit sehr kurzen rekursiven Turtle-Programmen zeichnen, da sie eine große Selbstähnlichkeit besitzen. Ein Baum, zum Beispiel, besteht letztendlich nur aus Zweigen. Ein Baum der Höhe 1 besteht aus nur einem einzigen geraden Zweig (Stamm). Ein Baum der Höhe 2 hat drei weitere, kürzere Zweige, die aus dem Baum der Höhe 1 entspringen. Bei Höhe 3 kommen wieder jeweils drei kürzere Zweige hinzu. Das gesuchte Programm hat also zwei Parameter, einen für die Höhe, an der abgebrochen werden soll, und einen für die aktuelle Länge des Zweigs. Schreiben Sie ein rekursives Turtle-Programm tree :: Int -> Double -> Turtle (), welches einen Baum dieser Art zeichnet. Hinweis: Obwohl das Programm tree schwieriger zu schreiben ist, als das Beispielprogramm nico, ist es dennoch ein paar Zeilen kürzer! Tipp: Achten Sie darauf, in welcher Position sich die Schildkröte vor einem rekursiven Aufruf befindet und in welcher Sie sie nach dem Aufruf wiederfinden. Stellen Sie nützliche Garantien bezüglich Position und Blickrichtung evtl. vor dem Rücksprung des Programms wieder her! Hinweis: Wie stark Sie die Länge der Zweige von einer Ebene zur nächsten verkürzen und wie Sie die Winkel wählen in denen Zweige abstehen, bestimmen maßgeblich das Aussehen des Baums! c) Eine andere selbstähnliche Figur ist das Sierpinski Dreieck. In der Datei “sierpinski.hs” finden Sie ein Turtle-Programm, welches es für verschiedene Stufen zeichnet: Es gibt auch hier eine wesentlich kürzere – wenn auch nicht einfachere – Variante das Sierpinski Dreieck anzunähern. Anstatt immer kleinere, unabhängige Dreiecke zu zeichnen, wird eine einzige Kurve dafür benutzt: Das Grundprimitiv auf Level 0 ist ein einfacher Strich. Level 1 baut drei dieser Striche zu einer Linkskurve mit 60 Grad Neigungen zusammen. Im allgemeinen baut das jeweils nächste Level drei Versionen des Vorlevels zu einer Links- oder Rechtskurve zusammen! Dabei wird die Drehrichtung der ersten und der dritten Version umgekehrt zur eigenen gewählt, die zweite Version dreht in dieselbe Richtung. Für die Abbildungen wurde jeweils die Anfangsdrehrichtung auf jedem Level alterniert, damit das Dreieck insgesamt richtig zum Betrachter ausgerichtet ist. Schreiben Sie ein Turtle-Programm sierpinski :: Integer -> Bool -> Turtle (), welches das aktuelle Level, sowie die aktuelle Drehrichtung nimmt und das Sierpinski-Dreieck als Kurve realisiert. Hinweis: Die korrekte Lösung ist schon mit wenigen, kurzen Zeilen möglich! d) Informieren Sie sich über die Drachenkurve und implementieren Sie sie als Turtle-Programm: e) Zeichnen Sie einen Farn: Hinweis: Das Rekursionsschema ist im Prinzip dasselbe wie bei Bäumen, jedoch sind die Parameter völlig anders gewählt und der Abbruch der Rekursion wird durch die Größe, anstatt die Höhe, herbeigeführt.