2. Elm
2.5 Rekursion
Mit Funkionen und Fallunterscheidungen haben Sie schon zwei der wichtigsten Prinzipien der Programmierung kennengelernt. Eine Sache haben Sie aber vielleicht bisher vermisst: Schleifen. Wenn Sie bereits in anderen Sprachen programmiert haben, dann sind Ihnen bestimmt Schleifen begegnet, also Anweisungen an das Programm, eine Abfolge von Schritten mehrfach zu wiederholen. Als Beispiel betrachten wir die Fakultätsfunktion (auf Englisch factorial):- Einen Funktionsaufruf wie
f 7
auszuwerten heißt, im Körper vonf
den Parameter durch den Ausdruck7
(das Argument) zu ersetzen. - Im Körper von Funktionen dürfen als Ausdrücke wiederum Funktionsaufrufe drinstehen.
Recursion.elm
im Order
PP/elm/src
.
Dann kopieren Sie folgenden Quelltext hinein.
module Recursion exposing (..)
factorial : Int -> Int
factorial n =
if n == 0 then
1
else
n * factorial (n - 1)
Der revolutionäre Gedanke steckt in Zeile 10. Dieser Ausdruck enthält seinerseits einen
Funktionsaufruf,
und zwar auf die Funktion selbst. Wir nennen dies Rekursion. Geht das / dürfen wir das?
Werten wir mal den Ausdruck
factorial 3
per Hand aus!
f 3
if 3 == 0 then 1 else 3 * factorial (3 - 1)
if False then 1 else 3 * factorial (3 - 1)
3 * (factorial (3 - 1))
3 * (factorial 2)
3 * (if 2 == 0 then 1 else 2 * factorial (2 - 1)
3 * (if False then 1 else 2 * factorial (2 - 1)
3 * 2 * factorial (2 - 1)
3 * 2 * factorial 1
3 * 2 * (if 1 == 0 then 1 else 1 * factorial (1 - 1))
3 * 2 * (if False then 1 else 1 * factorial (1 - 1))
3 * 2 * 1 * factorial (1 - 1)
3 * 2 * 1 * factorial 0
3 * 2 * 1 * (if 0 == 0 then 1 else factorial (0 - 1))
3 * 2 * 1 * (if True then 1 else factorial (0 - 1))
3 * 2 * 1 * 1
6
Im Prinzip steht nichts dagegen, im Körper von factorial
einen Ausdruck zu
schreiben,
der factorial
selbst wieder beinhaltet. Solange wir sicher sind, dass es
irgendwann "zu Ende geht". Entscheidend ist dabei unter Anderem, wie Elm eine
if
-Konstruktion auswertet, insbesondere die zeitliche Reihenfolge. Betrachten
wir noch einmal die Syntax von if
:
if condition then expression1 else expression2
Hierbei muss condition
ein Ausdruck vom Typ Bool
sein. Will Elm eine
if
-Konstruktion auswerten, so wird zuerst der Ausdruck condition
ausgewertet.
Ergibt dies True
, so wird expression1
ausgewertet; ergibt
es False
, dann wird expression2
ausgewertet.
Entscheidend ist, dass der andere Ausdruck ignoriert und gar nicht erst ausgewertet wird.
Dies rettet uns im Ausdruck
3 * 2 * 1 * (if 0 == 0 then 1 else factorial (0 - 1))Elm versucht nicht, den Ausdruck
expression2
, also factorial (0 - 1)
auszuwerten.
Dies würde nämlich dann in der Tat verhindern, dass wir jemals fertigwerden.
Rekursive Funktionen schreiben
- Überlegen Sie sich, für welche kleinen Werte von
Sie das Ergebnis kennen (für wäre das zum Beispiel oder . - Überlegen Sie sich, wie Sie
aus Ausdruck schreiben können, in dem vorkommt.
sumOfSquaresUpTo : Int -> Int
in Elm.
Gehen Sie wie folgt vor: offensichtlich gilt
if
-Ausdruck und schreiben
eine rekursive Funktion sumOfSquaresUpTo : Int -> Int
, die
exp : Int -> Int
, die Die Collatz-Vermutung
Wenn Sie für eine Funktion
Interessant wird es, wenn Sie mit einer Zahl anfangen, sagen wir
Klar, wenn wir einmal bei 1 ankommen, dann geraten wir in eine Dauerschleife
collatz : Int -> Int
, die
obige Funktion implementiert.
Tip. Da brauchen Sie keine Rekursion. Das geht mit einer einfachen Fallunterscheidung.
collatzUntilOne : Int -> Int
, die
zählt,
wie oft man die Funktion collatzUntilOne 1
0 : Int
collatzUntilOne 11
14 : Int
Tip. Wenn Sie in Ihrem Code durch 2 dividieren wollen,
schreiben Sie n // 2
und nicht n / 2
. Der Operator
/
will
Float
als Input und gibt Float
als Output, was in Ihrem
Kontext keinen Sinn macht.
Der Operator //
will Int
als Input und gibt als Output
ein Int
(das Ergebnis der Division, abgerundet). Beispielsweise:
4 / 2
2 : Float
floor (4/2)
2 : Int
floor (7/2)
3 : Int
Ihre Funktion ruft sich selbst auf, aber nicht mit
Rekursive Funktionen mit mehreren Parametern
Niemand verbietet Ihnen, Rekursion auch in Funktionen mit mehreren Eingabeparametern anzuwenden.
Überlegen wir uns mal, wie wir eine Funktion
hasDivisorBetween2andK 21 5
True : Bool weil 3 ein Teiler von 21 ist 3 <= 5
hasDivisorBetween2andK 77 6
False : Bool weil 7 der kleinste Teiler ist, und 7 > 6
hasDivisorBetween2andK 77 7
True : Bool weil 7 > 7 ist und das auch zählt
Um das irgendwie rekursiv zu implementieren, überlegen wir uns, ob wir den Wert von
Ein Teiler
Das können wir direkt in Elm-Code übersetzen:
hasDivisorBetween2andK : Int -> Int -> Bool
hasDivisorBetween2andK n k =
if k <= 1 then
False
else if modBy k n == 0 then
True
else
hasDivisorBetween2andK n (k - 1)
Um nun
prime : Int -> Bool
prime n =
n >= 2 && not (hasDivisorBetween2andK n (n - 1))
sqrt 17
4.123105625617661 : Float
floor (sqrt 17)
4 : Int
Aber das wäre nicht besonders instruktiv (und könnte für sehr hohe Zahlen hasDivisorBetween2andK
Rekursion über den zweiten Parameter