Algorithmen und Programmieren 1 Funktionale Programmierung - Musterlösung zu Übung 1 Dozent: Prof. Dr. G. Rote Tutoren: J. Fleischer, T. Haimberger, N. Lehmann, C. Pockrandt, A. Steen 18.10.2011 Ziele dieser Übung Grundlagen von Haskell lernen und verstehen. In dieser Übung sollte man folgende Konzepte geübt und verstanden haben: • Haskellprogramme schreiben, compilieren und ausführen • Kommentare schreiben • Signaturen schreiben Wahl guter Funktionsnamen Datentypen ∗ ∗ ∗ Int, Integer (ganzzahlige Werte) Float, Double (Gleitkommazahlen) Bool (Wahrheitswerte) • einfache Funktionen schreiben • Die Funktion: div • Die Funktion: mod • Die Funktion: max • Präx- und Inx-Schreibweise von Funktionen (div,mod) • Hilfsfunktionen Funktionen rufen Funktionen auf Weitergabe von Funktionsparametern 1 Aufgabe 1: Dreiecke Schreiben Sie eine Funktion zum Testen, ob x, y, z die Seitenlängen eines Dreiecks (mit positiver Flä- che) sind. (Entartete Dreiecke, wo zum Beispiel zwei Ecken zusammenfallen, sind ausgeschlossen.) Lösung zu Aufgabe 1: -- Prüft, ob 3 Kanten ein Dreieck bilden können. istDreieck1::Int->Int->Int->Bool istDreieck1 x y z = x + y > z && x+z>y && z+y>x && x>0 && y>0 && z>0 -- Prüft, ob 3 Kanten ein Dreieck bilden können. istDreieck2::Int->Int->Int->Bool istDreieck2 x y z = x+y+z > 2*(max (max x y) z) x > 0, y > 0, z > 0 istDreieck1 ist. Zusatzfrage: Beweisen Sie, dass man die drei Bedingungen Beweisen Sie, dass istDreieck2 äquivalent zu auch weglassen kann. Aufgabe 2: Negative Argumente Die zweistelligen Funktionen mod x y und div x y bestimmen den Rest von x bei Division durch y, beziehungsweise den ganzzahligen Quotienten der Division unter Vernachlässigung des Restes. a) Finden Sie durch Probieren heraus, was passiert, wenn für x und y auch negative Werte oder 0 eingesetzt werden. Lösung zu Aufgabe 2a: div div div div div mod mod mod mod mod (+7) (-7) (+7) (-7) (+2) (+8) (-8) (+8) (-8) (+2) (+2) (+2) (-2) (-2) ( 0) (+3) (+3) (-3) (-3) ( 0) = = = = = = = = = = (+3) (-4) (-4) (+3) Program error: divide by zero (+2) (+1) (-1) (-2) Program error: divide by zero b) Welche Beziehung gilt immer (mit wenigen Ausnahmen; mit welchen?) zwischen div x − mod x y x y? Lösung zu Aufgabe 2b: ∀x, y ∈ Z, y 6= 0 : x − mod x y = (div x y) · y c) Fassen Sie Ihre Ergebnisse zu Aufgabe a) in möglichst einfache Regeln. Lösung zu Aufgabe 2c: • y=0 • ist nie erlaubt. Das Ergebnis von mod xy hat immer das Vorzeichen von • |mod x y| < |y| • x y wird das Ergebnis y > 0 abgerundet (gegen −∞), y < 0 aufgerundet (gegen +∞). Bei div für für 2 y (oder es ist Null). und Aufgabe 3: Überlauf Im Gegensatz zum Typ Integer haben Gröÿen vom Typ Int einen Wert von höchstens auch 231 − 1 oder 2147483647. a) Was passiert, wenn bei einer Rechnung mit Gröÿen vom Type Int diese Grenze überschritten wird? (Bemerkung: Je nach Haskell-Umgebung oder Betriebssystem können Int-Werte auch bis zu 263 − 1 groÿ sein.) Lösung zu Aufgabe 3a: Sobald man das Maximum des Denitionsbereichs von Int überschritten hat, geht es bei dem Minimum des Denitionsbereichs von Int weiter. (2^31)-1 +0::Int 2147483647 (2^31)-1 +1::Int -2147483648 (2^31)-1 +2::Int -2147483647 b) Welches ist der kleinste Wert, der im Typ Int dargestellt werden kann? Lösung zu Aufgabe 3b: Mit dem folgenden Haskell-Code kann man die untere Grenze von Int bestimmen: minBound::Int -2147483648 Das Ergebnis ist −2147483648 oder auch −231 . Aufgabe 4: Zinseszinsberechnung Die folgende Funktion bestimmte die Zinsen einer Anlage von zinsfuÿ %. zinsen kapital zinsfuÿ = kapital * zinsfuÿ * 0.01 a) Denieren Sie unter Zuhilfenahme von zinsen eine Funktion endwert (mit geeigneten Parame- tern), die den Wert der Anlage (Kapital+Zinsen) am Ende einer Zinsperiode bestimmt. Lösung zu Aufgabe 4a: zinsen::Double->Double->Double zinsen kapi zfus = kapi * zfus * 0.01 -- Diese Funktion berechnet den Zinseszins nach einer Zeitperiode. endwert::Double->Double->Double endwert kapital zinssatz = kapital+(zinsen kapital zinssatz) b) Denieren Sie eine Funktion endwert2, die den Wert nach zwei Zinsperioden berechnet, wenn die Zinsen am Ende der ersten Periode wieder angelegt werden. Lösung zu Aufgabe 4b: -- Diese Funktion berechnet den Zinseszins nach zwei Zeitperioden. endwert2::Double->Double->Double endwert2 kapital2 zinsfuss2 = endwert (endwert kapital2 zinsfuss2) zinsfuss2 3 Zusatzlösung zu Aufgabe 4b: -- Diese Funktion berechnet endrekursiv den Zinseszins mit beliebiger Zeitperiode t endwertN::Double->Double->Integer->Double endwertN k z t | t == 0 = k | otherwise = endwertN (k+(zinsen k z)) z (t-1) c) Funktioniert es auch mit der folgenden Denition? zinsen kapital zinsfuÿ = kapital * zinsfuÿ / 100 Lösung zu Aufgabe 4c: Die Typklassen der Funktionen (*) und (/) unterscheiden sich zwar, :type (*) (*) :: Num a => a -> a -> a :type (/) (/) :: Fractional a => a -> a -> a da aber Float alle Eigenschaften beider Typklassen hat funktioniert es trotzdem. Aufgabe 5: Gleitkommarechnungen Vom mathematischen Standpunkt aus müsste die folgende Funktion immer 0 liefern: zero:: Float -> Float zero x = (1/x)*x - 1 Wegen Rundungsfehlern ist dies nicht immer der Fall. a) Finden Sie Werte x, bei denen das Ergebnis von 0 verschieden ist. -- Die Funktionen geben k Ergebnisse zurück, Lösung zu Aufgabe 5a: -- a) die die Funktion mit Float erfolgreich auf Abweichungen von 0 testen. zeroF::Float->Float zeroF x = (1/x )*x-1 zeroTesterF::[(Float,Float)] zeroTesterF k = take k [(n,(zeroF n)) | n<-[1.0,2.0..], (zeroF n) /= 0] Ergebnisliste für k=3: [(41.0,-5.960464e-008),(47.0,-5.960464e-008),(55.0,-5.960464e-008)] b) Ändert sich das Ergebnis beim Übergang zu Double? Lösung zu Aufgabe 5b: -- b) die die Funktion mit Float erfolgreich auf Abweichungen von 0 testen. zeroD::Double->Double zeroD x = (1/x )*x-1 zeroTesterD::[(Double,Double)] zeroTesterD k = take k [(n,(zeroD n)) | n<-[1.0,2.0..], (zeroD n) /= 0] Ergebnisliste für k=3: [(49.0,-1.11022302462516e-016),(98.0,-1.11022302462516e-016), (103.0,-1.11022302462516e-016)] Warum ist das Ergebnis nie positiv?? 4 Aufgabe 6: Gröÿer als der Durchschnitt Schreiben Sie eine Funktion von drei Integer-Parametern, die ausgibt, wie viele der Eingabeparameter gröÿer als der Durchschnittswert sind. Lösung zu Aufgabe 6: -- Gibt 1 b2i::Bool b2i True b2i False aus falls True, gibt 0 aus falls False. -> Integer = 1 = 0 -- Berechnet den Durchschnitt der 3 Eingabeparameter. -- Wichtig ist die Typumwandlung vor der Division avg::Integer->Integer->Integer->Double avg a b c = fromIntegral (a+b+c) / 3 -- Zählt wie viel der Eingabeparameter gröÿer als der Durchschnitt sind. biggerThanAVG::Integer->Integer->Integer->Integer biggerThanAVG x y z = b2i (fromIntegral x > avg x y z) + b2i (fromIntegral y > avg x y z)+ b2i (fromIntegral z > avg x y z) Diese Lösung kann versagen, wenn die Werte so groÿ sind, dass beim Umwandeln in eine Gleitkomma- x = 123123123123123123 und die Werte x, x, x + 1. x > (x + y + z)/3 um, indem man sie mit 3 multipliziert: zahl Rundungsfehler auftreten, zum Beispiel für Alternative: Man formt die Ungleichung 3x > x + y + z ⇐⇒ 2x > x + z biggerThanAVG':: Integer->Integer->Integer->Integer biggerThanAVG' x y z = b2i (2*x > y+z) + b2i (2*y > x+z)+ b2i (2*z > x+y) Alternative 2: Wenn man (a+b+c) 3), avg mit ganzzahliger Division deniert (zum Beispiel dann funktioniert es auch. Der Grund ist, dass für ganzzahliges x avg a b c = div α gilt und beliebiges x > α ⇐⇒ x > bαc Dabei steht α für den exakten Durchschnittswert, und bαc ist der abgerundete Wert von kleiner oder ≥ statt gröÿer würde diese Lösung nicht funktionieren. 5 α. Mit