TU Kaiserslautern - AG Softwaretechnik

Werbung
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.
Herunterladen