4. Listen
4.5 Listen verarbeiten mit map, filter, fold
Falls das letzte Kapitel Sie verunsichert hat und das Umformen in endrekursive Versionen Ihr Gehirn verknotet hat, so kommt nun Hilfe. Der der Realität lässt sich Rekursion tatsächlich in den meisten Fällen vermeiden, bzw. an eine Handvoll rekursiver Basisfunktionen "auslagern". Wir beginnen mit einer Übung, die Ihnen jetzt eigentlich leicht fallen sollte:
Übungsaufgabe Schreiben Sie folgende drei Funktionen (wenn Sie das nicht bereits getan haben):
incrementAll : List Int -> List Int, die jedes Element um 1 erhöhen soll;doubleAll, die jedes Element verdoppeln soll;labelAsPrime : List Int -> List (Int, Bool), die jede Zahl in der Eingabeliste mitTrue/Falseals Primzahl oder Nichtprimzahl labeln soll:labelAsPrime [3,4,5,6,7,8,9][(3,True),(4,False),(5,True),(6,False),(7,True),(8,False),(9,False)] : List ( Int, Bool )
Achten Sie nicht auf Endrekursion, denn darum geht es hier nicht.
Ist Ihnen etwas aufgefallen? Sie haben im Prinzip dreimal fast den gleichen Code geschrieben. Denn alle drei Funktionen machen fast das gleiche: sie wenden eine Funktion auf jedes Element in einer Liste an. Kann man das auslagern? Ja, wenn man Funktionen als Eingabewerte zulässt:
applyToEach : (a -> b) -> List a -> List bapplyToEach f list =case list of[] ->[]x :: rest ->f x :: applyToEach f rest
Jetzt brauchen Sie sich für incrementAll, doubleAll, labelAsPrime
keinen rekursiven Code mehr schreiben. Sie müssen einach applyToEach richtig
aufrufen:
labelAsPrime : List Int -> List ( Int, Bool )labelAsPrime list =letlabelOneNumber : Int -> ( Int, Bool )labelOneNumber n =( n, isPrime n )inapplyToEach labelOneNumber list
Übungsaufgabe
Schreiben Sie eine endrekursive Version von applyToEach.
Sie sollten locker Listen der Länge 20000 damit verarbeiten können.
Ab jetzt müssen Sie nicht mehr Ihre selbst geschrieben Funktion applyToEach
verwenden, sondern dürfen die vorgefertige Funktion List.map verwenden:
List.map<function> : (a -> b) -> List a -> List b
Ähnlich zur Funktion applyToEach bzw. List.map
ist die Funktion keepOnlyThoseWith, die aus einer Liste
nur diejenigen Elemente übernimmt, die eine bestimmte Bedingung erfüllen,
beispielsweise
keepOnlyThoseWith isPrime [1,2,3,4,5,6,7,8,9][2,3,5,7] : List Int
Übungsaufgabe
Implementieren Sie keepOnlyThoseWith.
Auch hier gibt es die bereits vorgefertigte Funktion
List.filter:
List.filter<function> : (a -> Bool) -> List a -> List aList.filter Char.isLower (String.toList "Guten Morgen")['u','t','e','n','o','r','g','e','n'] : List Char
Die Funktionen List.foldl
und List.foldr
Wir kommen nun zur konzeptuell schwierigsten Listenfunktion. Als motivierendes Beispiel wollen wir alle Zahlen einer Liste aufaddieren:
addAll [1,3,4,5]13
Hier ist der dazugehörige Code:
addAll : List Int -> IntaddAll list =case list of[] ->0x :: rest ->x + addAll rest
und zwei Testaufrufe:
addAll (List.range 1 1000)500500addAll (List.range 1 10000)RangeError: Maximum call stack size exceeded
Tja, der Code von addAll ist natürlich
nicht endrekursiv, daher scheitert er bereits an
moderat langen Listen.
Übungsaufgabe
Schreiben Sie eine endrekursive Funktion
addAll2.
Meine Lösung
Das geht natürlich wieder mit der berühmten Zwischenergebnisvariable:
addAll2 : Int -> List Int -> IntaddAll2 start list =case list of[] ->startx :: rest ->addAll2 (start + x) rest
Und jetzt gehen große Listen:
addAll2 0 (List.range 1 10000)50005000 : Int
Ganz allgemein wollen wir die Möglichkeit haben
- mit einem Startwert
startvom Typbzu beginnen (hier: 0); - die Listenelemente, die vom Typ
asind, eins nach dem anderen mit einer Kombinatorfuktioncombine(hier:+) auf das Zwischenergebnis "draufaddieren" und einen neuenb-Wert zu erhalten; - das Endergebnis zurückzugeben.
Hier ist mein Code:
combineAll : (a -> b -> b) -> b -> List a -> bcombineAll f start list =case list of[] ->startx :: rest ->f x (combineAll f start rest)
Und hier wieder die endrekursive Version:
combineAll2 : (a -> b -> b) -> b -> List a -> bcombineAll2 f start list =case list of[] ->startx :: rest ->combineAll2 f (f x start) rest
Ein kleines Detail: die Funktionen
combineAll und combineAll2
wollen als erstes Argument die Kombinatorfunktion
f. Wenn wir also beispielsweise
alle Zahlen aufaddieren wollen, brauchen wir
eine Funktion, die zwei Zahlen addiert.
Die gibt es schon: +. Allerdings geht
+ mit Infixschreibweise, also
2 + 3 statt + 2 3, die
Funktion f steht in Zeilen 272 bzw. 282
allerdings in Präfixnotation. In Elm
gibt es die Funktion (+), die nichts
anderes ist als + als normale
Präfix-Funktion. Daher also:
(+)<function> : number -> number -> number(+) 3 58 : numbercombineAll (+) 0 [1,2,3,4]10combineAll2 (+) 0 [1,2,3,4]10combineAll (+) 0 (List.range 1 10000)RangeError: Maximum call stack size exceededcombineAll2 (+) 0 (List.range 1 10000)50005000 : Int
Die Version combineAll2 ist also
besser, weil sie endrekursiv ist.
Schauen wir uns ein zweites Beispiel an. Wir
wollen eine List Int als
Dezimalzahl interpretieren und in ein Int
umwandeln, also
listToDecimal [1,5,3,7]1537 : Int
Wir kombinieren ein "Zwischenergebnis" s, beispielsweise
153, mit einem neuen Listenelement x
(beispielsweise 7), indem wir
10* + x berechnen. Wir müssen uns nur
daran erinnern, dass die Kombinatorfunktion f
zuerst das Listenelement will (vom
Typ a), dann das Zwischenergebnis
(vom Typ b):
comb: Int -> Int -> Intcomb x s = x + 10*scombineAll2 comb 0 [1,5,3,7]1537 : Int
Schematisch passiert das folgende:
Das Problem ist nun, dass combineAll2
und combineAll nicht mehr das gleiche tun:
combineAll2 comb 0 [1,5,3,7]7351 : Int
In diesem Fall scheint das Ergebnis von
combineAll2 zu stimmen, allerdings
gibt es gute Gründe, große Zahlen so darzustellen,
dass die "kleinen" Stellen am Anfang der Liste kommen.
Schematisch gesehen tun combineAll das hier:
Beide Funktionen sind nützlich, je nach Kontext.
Es gibt sie bereits unten den Namen
List.foldl und List.foldr.
Nochmal zur Übersicht:
Die Funktion List.foldl
Die Funktion List.foldr
Übungsaufgabe
Schreiben Sie eine endrekursive Version von
combineAll. Achtung: die Funktion
combineAll2 ist zwar endrekursiv,
tut aber Anderes! Die Lösung ist etwas trickreicher
(oder dümmer, je nach Sichtweise).
Übungsaufgabe
Schreiben Sie eine Funktion leftToRightMaxima,
die für eine Liste die "bisher größten" Elemente rausfiltern.
Beispiel:
leftToRightMaxima [1,3,6,2,3,8][1,3,6,8]
Die Elemente 1, 3, 6 werden übernommen, weil jedes von ihnen
das bisher größte war. Das vierte Element, also 2, wird nicht übernommen,
weil wir bereits etwas größeres gesehen haben (zum Beispiel 3 oder 6).
Verwenden Sie keine Rekursion, sondern nur vorgefertige Funktionen
wie List.map, List.filter, List.foldl, List.foldr, List.reverse.
Tip: kriegen Sie Ihre Datentypen auf die Reihe.
Was ist a, der Typ Ihrer Eingabelistenelemente?
Das ist Int. Was ist b, der Rückgabewert
von List.foldl und List.fodlr und somit
wohl Ihr Rückgabewert? Wir wollen als Ergebnis eine Liste,
also ist b der Type List Int.
Somit ist klar, wie die Kombinatorfunktion auszusehen hat:
f : Int -> List Int -> List Int
Ein Aufruf von f hat also das Schema
f neuesElement jetzigeErgebnisListe.
Jetzt müssen Sie sich überlegen, was f sein soll.
Übungsaufgabe Schreiben Sie eine Funktion, die die Elemente einer Liste durchnumeriert:
numberItems 1 ["monday", "tuesday", "wednesday"][(1, "monday"), (2, "tuesday"), (3, "wednesday")]
Gehen Sie systematisch vor. In der Signatur von
List.foldl : <function> : (a -> b -> b) -> b -> List a -> b,
was ist in Ihrem Fall a und was ist b?
Übungsaufgabe
Re-implementieren Sie die Funktionen List.map und
List.filter
ohne Rekursion, nur mithilfe der Funktionen List.foldl und
List.foldr.
Übungsaufgabe
Implementieren Sie eine endrekursive Funktion loop n f x, die
die Funktion \(f\) \(n\)-mal auf einen Startwert anwendet.
add_x : String -> Stringadd_x s = s ++ "x"loop 10 add_x "hello""helloxxxxxxxxx" : String