Funktionale Programmierung mit Haskell und F#

Werbung
3. Funktionale Programmiersprachen
Haskell und F#
Einleitung
Was haben die zuletzt behandelten vier Programmiersprachen gemeinsam?
Wahrscheinlich fällt einem zu dieser Frage zunächst nicht viel mehr ein, dass es halt
Programmiersprachen sind. Zu verschieden erscheinen sie einem, als dass man eine
weitergehende Gemeinsamkeit entdecken könnte.
Und dennoch gibt es sie: Es sind alles imperative Programmiersprachen! Ein Befehl
nach dem anderen wird in festgelegter Reihenfolge abgearbeitet. Gewiss, es gibt die
Schleifen und bei schlechter Programmierung auch die Sprungbefehle. Das ändert
aber nichts daran, dass die Reihenfolge festgelegt ist.
Zum besseren Verständnis sollten Sie sich den Abschnitt Klassifikation nach den
Programmierparadigma in Kapitel 2.0 (S.75) nochmals durchlesen.
Das imperative Paradigma passt sehr genau zur klassischen Von-NeumannArchitektur für Rechner. Es ist relativ leicht zu verstehen und wird daher besonders
von Programmier-Novizen bevorzugt.
Der Befehl (aus Delphi) x:= x + 5 ist mathematisch gesehen reiner Unfug!
Subtrahieren Sie auf beiden Seiten x, dann bekommt man die Aussage: 0 = 5.
So kann es also nicht gemeint sein. In der Tat soll die obige Schreibweise etwas
ganz anderes heißen: Der Speicherinhalt von x wird mit 5 addiert und wieder auf den
Speicher von x geschrieben.
Einen solchen Befehl werden Sie in Haskell oder F# vergeblich suchen. Und das liegt
daran, dass Haskell und F# funktionale Programmiersprachen sind. Und
funktionale Programmiersprachen ruhen auf einem völlig anderen Programmierparadigma:
Programme bestehen hier ausschließlich aus einer Vielzahl von Funktionen, daher
der Name. Das Hauptprogramm ist eine Funktion, welche die Eingabedaten als
Argument erhält und die Ausgabedaten als seinen Wert zurückliefert. Diese
Hauptfunktion verwendet in ihrer Definition üblicherweise weitere Funktionen, die
wiederum ihrerseits weitere Funktionen verwenden, und das geht so weiter, bis
irgendwann, am Boden der Aufrufhierarchie ankommend, nur noch die
Grundfunktionen der Programmiersprache verwendet werden, wie etwa die Addition.
(Wikipedia)
Alles klar?!? Vermutlich nicht!
Man kann Obiges auch etwas ausführlicher darstellen. Das soll in den nächsten
Abschnitten versucht werden.
1
Ein Grund für Verwirrung ist sicher der Umstand, dass der Begriff Funktion auch in
imperativen Programmiersprachen verwendet wird. Nur hat er da eine ganz andere
Bedeutung, wie in der Mathematik:
Funktionen sind in Delphi, Java etc so etwas wie Unterprogramme oder Prozeduren.
In Mathematik ist eine Funktion eine eindeutige Zuordnung von einer Menge in eine
andere Menge.
Beispielsweise ordnet die Funktion f: x → x 2 jedem Element x aus der
Definitionsmenge (zum Beispiel den rationalen Zahlen) das Quadrat von x zu.
Natürlich ist dies eine Funktion, denn die Zuordnung ist eindeutig ( 5 wird eindeutig
25 zugeordnet).
Bei der rein funktionalen Programmierung ist dieser mathematische
Funktionsbegriff gemeint! Der Vorteil: Man kann jetzt die sehr umfangreich
behandelten Methoden der Mathematik nutzen, da man ja auch die gleiche
Funktionsdefinition benutzt.
Selbst manche Beweise der Mathematik können der funktionalen Programmiersprache dienlich sein. Wer hätte das gedacht?
Programme bestehen dort, vereinfacht ausgedrückt, aus Funktionen, die weitere
Funktionen aufrufen. Das bedeutet vor allem, dass Funktionen selbst als
Übergabewert (Parameter) dienen können. Sie verhalten sich daher mehr oder
weniger wie Datentypen aus der imperativen Programmiersprache.
Einige Konsequenzen:
•
•
•
•
Es gibt keine Schleifen! Sie werden durch Rekursionen ersetzt!
Es gibt keine Variablen! Sie werden durch Parameter ersetzt!
Funktionen selbst sind Werte!
Das Resultat einer Funktion hängt nur vom Parameterwert ab!
Die Rekursion sollten Sie aus der Mathematik kennen. Wenn Ihnen das Thema nicht
mehr präsent sein sollte, hier ein kleines Beispiel (natürlich aus der Informatik):
Wie würde man etwa in Delphi einen Programmtext zur Berechnung der Fakultät
aufschreiben?
Eine Möglichkeit:
function TFAnwendung.Fakultaet(wZahl:word):longint;
var liProdukt: longint;
i : word;
begin
liProdukt:=1;
for i:=1 to wZahl do
liProdukt:=liProdukt*i;
Fakultaet:=liProdukt;
end;
Kleine Frage am Rande: Welche Bedeutung hat hier der Begriff function ?
2
Löst man das Problem mit Hilfe einer Rekursion, so sieht der Quelltext (in Haskell) so
aus:
fak 0 = 1
fak n = n * fak (n-1)
Ist doch beeindruckend, der Unterschied! Und wenn man dann noch bedenkt, dass
mit dem Delphiprogramm nur maximal 16! berechnet werden kann, während bei
Haskell keine theoretische Grenze für n existiert! Man kann dort problemlos 38456!
ausrechnen. Versuchen Sie das mal in C#, Delphi, Java oder PHP zu realisieren!!
(Wir werden auf das Beispiel zurückkommen):
Aufgabe 1
Realisieren Sie das oben angedeutete Fakultiäts-Programm in einer beliebigen
imperativen Programmiersprache. (Delphi ist hier bequem, weil der Quellcode schon
fast vollständig ist! Programmordner: FakultaetImperativ )
Man darf jetzt aber nicht glauben, dass imperativen Programmiersprachen die
Rekursion fremd ist. Um das nicht zu vergessen, lösen Sie die nächste Aufgabe!
Aufgabe 2
Versuchen Sie dies mal das Fakultätsproblem in der von Ihnen oben verwendeten
Programmiersprache rekursiv zu lösen! (Programmordner: FakultaetRekursiv)
Versuchen Sie es zunächst ohne Hilfe. Wenn Sie nicht weiterkommen, dann hilft ein
Blick auf die folgenden Zeilen:
function Fakultaet(n: longint):longint;
begin
if n=0 then Fakultaet:= 1
else Fakultaet:= Fakultaet(n-1)*n;
end;
Schreiben Sie von Hand die Schritte auf, die der Rechner zum Beispiel bei n = 4 zu
durchlaufen hat! Das ist sehr lehrreich – und nebenbei die beste Methode,
Rekursionen zu verstehen!
Übrigens: Funktionale Programmiersprachen, wie LISP, Miranda und Haskell,
gehören zur Gruppe der deklarativen Programmiersprachen.
LISP gibt es schon seit Ende der 50-er Jahre. Die Sprache wurde besonders in der
Forschung der künstlichen Intelligenz verwendet. Heute verwendet sie kaum mehr
jemand mehr.
3
Haskell
Grundlagen
Den Name verdankt die Sprache dem Mathematiker Haskell Brooks Curry. Er
forschte Anfang der 40-er Jahre des letzten Jahrhunderts auf dem Gebiet der
kombinatorischen Logik. Diese Theorie bildet die Grundlage und Voraussetzung
für funktionale Programmiersprachen.
Curry muss schon sehr bedeutendes geleistet haben, wenn man eine Programmiersprache mit seinem Vornamen bezeichnet!
Wenn Sie wissen wollen, womit sich Curry sonst noch beschäftigt hat, so lesen Sie
zum Beispiel den gut verständlichen Artikel über Currys Paradox bei Wikipedia:
http://de.wikipedia.org/wiki/Currys_Paradoxon
Was Wikipedia sonst noch zur Geschiche von Haskell weiß, lesen Sie hier:
Gegen Ende der 1980er Jahre gab es bereits einige funktionale Programmiersprachen, alle mit ihren Vor- und Nachteilen. Um der Wissenschaft eine einheitliche
Forschungs- und Entwicklungsbasis bereitzustellen, sollte eine standardisierte und
moderne Sprache die funktionale Programmierung vereinheitlichen. Zunächst wollte
man dazu Miranda als Ausgangspunkt benutzen; doch deren Entwickler waren
daran nicht interessiert. So wurde 1990 Haskell 1.0 veröffentlicht.
Die aktuelle Version der Programmiersprache ist eine überarbeitete Variante des
Haskell-98-Standards von 1999. Haskell ist die funktionale Sprache, an der zur Zeit
am meisten geforscht wird. Demzufolge sind die Sprachderivate zahlreich; dazu
zählen Parallel Haskell, Distributed Haskell und sogar objektorientierte Varianten
(Haskell++, O'Haskell, Mondrian). Des Weiteren diente Haskell beim Entwurf neuer
Programmiersprachen als Vorlage. So wurde z.B. im Falle von Python die LambdaNotation sowie Listenverarbeitungssyntax übernommen.
(28.10.2007)
Wenn Sie jetzt noch wüssten, was die Lambda-Notation ist...
In Haskell sieht sie so aus:
plus a b = a + b
Das bedeutet: plus ist eine Funktion (allgemein Lambda genannt), die auf a und b
angewandt wird. Das Ergebnis der Funktion ist a + b .
So einfach ist das!!
Frage: Woher bekommt man eigentlich die Programmiersprache Haskell?
Antwort: Wir verwenden hier den Haskell-Interpreter Hugs: http://haskell.org/hugs/
4
Das Programm ist Freeware und kann auch vom Tauschverzeichnis auf den USBStick geladen werden.
Klicken Sie im NAL unter Informatik das Ikon für Haskell doppelt, so sehen Sie
dieses Fenster:
Hier wurde allerdings bereits, dem Vorschlag am „Prompt“ entsprochen, :?
einzugeben. Daraufhin bekommt man einige Kommandos gezeigt.
Ein wenig enttäuscht sind Sie schon, stimmts?! Soll das magere Editierfeld alles
sein? Warten Sie es ab!
Öffnen Sie mit file / Modulmanager das folgende Fenster:
5
Offensichtlich gibt
es hier bereits
geladene Module,
die bereits jetzt
eine Fülle von
Funktionen
besitzten. Um dies
genauer zu
untersuchen,
öffnen Sie einen
der Module mit
dem Button „Edit“
auf der linken
Seite. Die Einträge
sollte man aber
keinesfalls
verändern.....
Wir wollen jetzt in einem ersten Schritt Haskell
unsere eigene Addition von oben beibringen (plus a
b = a+b)
Öffnen Sie mit dem einfachen Windows-Editor ein
neues Dokument und schreiben Sie lediglich obige
Zeile. Danach speichern Sie das Dokument unter
addition.hs ab.
Mit dem obigen Modulmanger können Sie danach
Ihr erstes Modul laden (add) und danach im
Hauptfenster auch benutzen (siehe Bild links).
Interessant wäre jetzt zu wissen, was Haskell mit
plus a b anfängt.
Problieren Sie weitere Möglichkeiten aus!
Die erste Programmzeile, die wir geschrieben haben (plus a b = a+b) ist für den
Interpreter eine echte Herausforderung. Es wird nämlich nicht festgelegt, welchen
Typs die Parameter a und b sein sollen. Und wenn man ein Charakteristikum von
Haskell nennen müsste, dann ist es mit Sicherheit die gnadenlose Typenkontrolle!
Diesbezüglich ist Haskell das genaue Gegenteil von PHP!
Machen wir es also richtig:
--plus eigen (Keine Großbuchstaben!!)
plus :: Double->Double->Double
plus a b = a + b
Man kann hier auch erkennen, wie ein Kommentar geschrieben werden kann.
6
Die zweite Zeile entspricht der Schreibweise des Lambda-Kalküls. Jetzt allerdings
muss man bei der Eingabe des Parameters auch korrekt vorgehen. Dabei geht die
Zeile plus 44 58 noch in Ordnung. Damit aber auch alles klar ist bekommt das
Ergebnis einen Dezimalpunkt und eine Null spendiert!
Die Funktion plus_i unterscheidet sich
von der Funktion plus nur dadurch, dass
statt Double Integer verwendet wurde.
Daher muss die nebenstehende zweite
Rechnung zu einer Fehlermeldung führen.
Auch wenn formal 44.0 mit etwas gutem
Willen als Integer durchgehen kann.
Integer (nicht zu verwechseln mit Int) scheint ein ganz besonderer Datentyp zu sein.
Sehen Sie sich die unten abgebildete Ganzzahl-Addition etwas genauer an:
Der zweite Summand hat 47 Dezimalstellen. Versuchen Sie es ruhig mal mit 200
oder 300 Stellen.....
Zeit für einige wichtige
Informationen
•
In Haskell muss man zwischen Groß- und Kleinschreibung unterscheiden!
Also plus ist nicht gleich Plus!!
•
Wenn man nicht weiß, welchen Typ eine Funktion hat, dann kann man dies
mit :t ganz einfach herausbekommen. Zum Beispiel:
Das geht natürlich auch mit in Hugs vordefinierten Funktionen:
Im Gegensatz zu den ersten Beispielen ist hier kein Parameter-Typ
vorgeschrieben. const ist also offensichtlich eine Funktion, die auf zwei
beliebige Paramter wirkt und ein Ergebnis vom ersten Typ liefert!
Probieren Sie das gleich mal aus. Z.B.:
Alles klar?
•
Wenn man eine Funktion definieren will, muss man nicht immer über den
Umweg des Skriptes (mit Endung .hs) gehen. Man kann auch eine nur für eine
7
Zeile gültige Funktion schreiben:
Der „Backslash“ \ steht für Lambda!
Bevor Sie nun selbst eigene „Kurzzeitfunktionen“ erstellen, sehen Sie sich
diese Zeilen links noch an:
Einmal geht’s einmal nicht! Weshalb?
•
Will man die „Kurzzeitfunktion“ weiterverwenden,
so schreibt man sie wieder in ein Skript (Endung
.hs), das man mit dem Modulmanager einliest.
Aber Achtung:
Das hätten Sie nicht vermutet, stimmt’s!? Der Interpreter geht davon aus wird ihm nichts anderes mitgeteilt - dass es sich bei den Parametern x und
y um Integer handelt. Sehr streng, dieser Haskell-Interpreter!!
Aufgabe 3
Schreiben Sie als „Festfunktion“ :
•
Eine eigene Quadratfunktion „quadrat“
•
Die Identitätsfunktion „identitaet“ für beliebige Parameter
(Das ist die Funktion, die jedem Wert den selben Wert zuordnet)
•
Eine Funktion „nimm2von2“, die von zwei eingegebenen Parametern den
zweiten Wert zuordnet, - und zwar für beliebige Parameter.
Nun muss man ja nicht unbedingt die Funktionen, die in Haskell schon vorhanden
sind, erneut programmieren. Daher hier eine Zusammenstellung arithmetischer
8
und logischer Operatoren und zudem einige höchst praktischer arithmetischen
Funktionen, die durch das Modul Prelude automatisch geladen werden:
•
•
•
•
•
+
*
/
^
•
div ; mod (ganzzahlige Division bzw.
Restbildung):
Addition
Subtraktion
Multiplikation
Division
Potenz
div :: Integral a => a -> a -> a
mod :: Integral a => a -> a -> a
2+3
2-3
2*3
2/3
2^3
=5
= -1
=6
= 0.666667
=8
z.B.:
(Achtung: div und mod stehen zwischen zwei Accents graves!)
•
gcd ; lmc (größter gem.Teiler bzw. kleinstes gem.
Vielfaches):
gcd :: Integral a => a -> a -> a
lcm :: Integral a => a -> a -> a
•
z.B.:
even ; odd (gerade bzw. ungerade Zahl):
even :: Integral a => a -> Bool
odd :: Integral a => a -> Bool
z.B.:
(Erläuterung: DieTypklasse Integral besteht aus den Typen Int und Integer.)
•
•
•
&&
||
not
logisches UND
logisches ODER
logische Verneinung
2<3 && 3<4
True||False
not (pi<3)
= True
= True
= True
Wenn Sie die obigen Definitionen aufmerksam gelesen haben, ist Ihnen sicher die
Schreibweise
even :: Integral a => a -> Bool aufgefallen. Was bedeutet Integral a => ?
Haskell unterscheidet drei verschiedene Arten von Funktionen:
9
•
Monomorphe Funktionen, die nur genau einen Eingabetyp akzeptieren:
plus :: Double->Double->Double
plus a b = a + b
• Polymorphe Funkionen, die beliebige Eingabetypen akzeptieren:
nimm1von2 :: a -> b -> a
nimm1von2 a b = a
• Überladene Funktionen, die mehrere Eingabetypen akzeptieren:
quadrat :: Num a => a -> a
quadrat a = a*a
Die Funktion quadrat lässt sich natürlich nur dann überladen, wenn die Operation auf
der erlaubten Klasse, hier Num, definiert ist. Wenn man versucht, die
Quadratfunktion polymorph zu definieren, wird man unweigerlich eine Fehlermeldung
bekommen:
quadratf :: a -> a
quadratf a = a*a
ERROR ….Inferred type is not general enough
Rekursion
Haskell ist eine funktionale Programmiersprache. Daher gibt es keine Schleifen,
sondern Rekursionen, wie wir weiter oben festgestellt haben. In Aufgabe 2 wurde
ohne mit Delphi dargestellt, wie man beispielsweise die Berechnung von n! rekursiv
bewältigen kann. Auf Seite 162 steht der Programmcode für Haskell. Mit einer
Kommentarzeile und und der Festlegung des Definitionsbereichs sieht es nun so
aus:
-- Definition Fakultät
fak :: Integer -> Integer
fak 0 = 1
fak n = n * fak (n-1)
Einfacher geht nicht! Testen Sie die Programmzeilen! Was ist 8872! ? Und jetzt
probieren Sie das mal mit Ihrem GTR....
In Klasse 12 werden Sie von der Fibonacci-Folge gehört haben oder hören. Und
das steckt dahinter:
Fibonacci illustrierte diese Folge durch die einfache mathematischen Modellierung
des Wachstums einer Kaninchenpopulation nach folgender Vorschrift:
10
1.
2.
3.
4.
Zu Beginn gibt es ein Paar geschlechtsreifer Kaninchen.
Jedes neugeborene Paar wird im zweiten Lebensmonat geschlechtsreif.
Jedes geschlechtsreife Paar wirft pro Monat ein weiteres Paar.
Die Tiere befinden sich in einem abgeschlossenen Raum („in quodam loco,
qui erat undique pariete circundatus“), so dass kein Tier die Population
verlassen und keines von außen hinzukommen kann.
Das erste Paar erzeugt seinen Nachwuchs bereits im ersten Monat.
Jeden Folgemonat kommt dann zu der Anzahl der Paare, die im letzten Monat gelebt
haben, eine Anzahl von neugeborenen Paaren hinzu, die gleich der Anzahl der
Paare ist, die bereits im vorletzten Monat gelebt haben, da genau diese
geschlechtsreif sind und sich nun vermehren.
Fibonacci führte den Sachverhalt für die zwölf Monate eines Jahres vor (1, 2, 3, 5, 8,
13, 21, 34, 55, 89, 144, 233, 377) und weist auf die Bildung der Reihe durch Addition
mit dem jeweils vorhergehenden Reihenglied hin (1+2=3, 2+3=5, 3+5=8, etc.).
Er merkte außerdem an, dass die Folge sich − unter der Annahme unsterblicher
Kaninchen − unendlich fortsetzen lässt: „et sic posses facere per ordinem de infinitis
numeris mensibus.“
Weitere Beachtung hatte er dem Prinzip in seinen erhaltenen Werken nicht
geschenkt.
(Wikipedia 11.2007)
Zwei ganz andere Zugänge zu der Fibonacci-Folge finden Sie hier:
http://www.matheprisma.uni-wuppertal.de/Module/Rekurs/index.htm (Fibonacci)
Mathematisch ausgedrückt ist der Sachverhalt klarer:
f(1) = 1;
-- im ersten und zweiten Monat gibt es ein Paar
f(2) = 1
f(n) = f(n - 1) + f(n - 2) für n > 2 ;
Das obige Verfahren, bei dem vom Interpreter zur Laufzeit zunächst einige
Sonderfälle und dann ein allgemeiner Fall geprüft werden, nennt man Patternmatching.
Das Ganze erinnert Sie vielleicht entfernt an die if-then-else- Abfragen. Und so falsch
liegen Sie da auch nicht. Wie dort kann man hier sehr leicht in eine Endlosschleife
einsteiten!
Aufgabe 4
Schreiben Sie ein Haskell-Programm, das fib(n) , also die Anzahl der Paare nach n
Monaten berechnet. Wieviele Paare gibt es nach 23 Monaten? (Keine viel höheren
Werte verwenden, wenn Sie nicht Stunden oder Tage warten wollen....)
11
Sehr lange braucht das Programm für n = 34. fib(34) = 5702887. Eine guter
Benchmark für Ihren Prozessor!!
Das geht entschieden zu langsam! Nicht weil Haskell nichts taugt, sondern weil der
Programmcode zwar einfach zu schreiben, aber höchst anspruchsvoll für Prozessor
und RAM ist.
Das geht viel, viel besser und schneller, wenn man erst einmal Listen in Haskell
behandelt hat.
Guards (Wächter)
Auch wenn mit Pattern-matching unterschiedliche Reaktionen des Programms
erreicht werden können, so kann es die if-then-else-Konstruktion nicht ersetzten.
Diese gibt es zwar auch in Haskell (und wird, wie es sich gehört als Funktion
aufgefasst), sind allerdings in funktionalen Programmiersprachen nur zweite Wahl.
Denn es gibt etwas viel besseres: die Guards.
Diese „Wächter“ werden durch senkrechte Striche eingeleitet und stehen alle
untereinander. Ein End-Wächter wird durch otherwise aufgerufen.
Ein Beispiel:
funktionspezial n
| n == 1
| mod n 2 == 0
| otherwise
= 5
= 7
= 10
-- Wächter 1
-- Wächter 2
-- Wächter 3
Erklärung:
Wenn n =1 , dann wird 5 ausgebeben; ist n durch 2 teilbar, so wird 7 ausgegeben. In
allen anderen Fällen bekommt man den Wert 10.
Hier die wichtigsten Vergleichsoperatoren:
==
/=
<
<=
>
>=
gleich
ungleich
kleiner
kleiner oder gleich
größer
größer oder gleich
"Eis" == "eis"
2 /= 3
"Haus" < "haus"
4 <= sqrt 16
'a' > 'A'
3^2 >= 0
= False
= True
= True
= True
= True
= True
Aufgabe 5
Wenn Sie nicht mehr wissen, wie die Schaltjahre berechnet werden, so sehen lesen
Sie sich die Erklärung in Wikipedia durch:
•
Ist die Jahreszahl durch 4 teilbar, aber nicht durch 100, dann ist es ein
Schaltjahr mit 366 Tagen. Beispiele: 1980, 1972, 1720.
12
•
•
Ist die Jahreszahl durch 100 teilbar, aber nicht durch 400, dann ist das Jahr
ein gewöhnliches Gemeinjahr und hat nur 365 Tage, z. B. in den Jahren 1700,
1800 und 1900 oder ferner 2100.
Ist die Jahreszahl durch 400 teilbar, ist das Jahr ein Schaltjahr. Die Jahre
1600 und 2000 waren – in Übereinstimmung mit der Julianischen Schaltregel
– Schaltjahre zu 366 Tagen.
Versuchen Sie nun mit dieser Definition den Programmcode zur Entscheidung, ob
ein Jahr y ein Schaltjahr ist oder nicht, zu verstehen:
-- Bestimmung Schaltjahr
schaltjahr :: Int -> Bool
schaltjahr y
| mod y 100 == 0 =
| otherwise
=
(mod y 400 == 0)
(mod y 4 == 0)
-- (1)
-- (2)
-- (3)
-- (4)
Listen
Wen wundert es, wenn funktionale Programmiersprachen ihren Hang zur Rekursion
auch auf die Datenstrukturen selbst beziehen wollen?
Das beste Beispiel für solche rekursiv definierten Datenstrukturen sind die Listen.
In Haskell sieht das so aus:
[]
[element]
(x:xs)
-- leere Liste
-- Liste mit einem Element
-- nicht leere Liste, x ist erstes Element, xs ist die Restliste
In Listen sind alle nur denbaren Datentypen als Elemente zugelassen, aber alle
Elemente müssen den selben Typ haben. Die meisten Listenfunktionen sind
polymorph, d.h., sie funktionieren mit allen Listen und sind nicht an einen Typ
gebunden.
Hier einige Beispiele:
[4,6,7,9] :: [Int]
['H','a','l','l','o'] = "Hallo"
["Lehrer","sind","auch",“Menschen!“] :: [[Char]] (oder :: [String] )
(1>5):[True, False] = [False, True, False]
5^2 : 6^2 : 3^2 : 1^2 : [ ] = [25, 36, 9, 1]
Es gibt auch einige Vereinbarungen bezüglich der Vereinfachung der Schreibweise:
[20 .. 25]
[20.5 .. 26]
[2, 4 .. 12]
[5, 4 .. 1]
= [20, 21, 22, 23, 24, 25]
= [20.5, 21.5, 22.5, 23.5, 24.5, 25.5, 26.5]
= [2,4,6,8,10,12]
= [5, 4, 3, 2, 1]
13
[5.1, 5.2 .. 6.0]
= [5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0]
[5.1+i*0.1 | i<-[0..9]] = [5.1,5.2,5.3,5.4,5.5,5.6,5.7,5.8,5.9,6.0]
(die obige Schreibweise erinnert zwar sehr an eine Schleife, ist aber, wie jede
Liste in Haskell, durch eine Rekursion entstanden. Siehe Erklärung weiter
unten)
[1 .. ]
= [1, 2, 3, 4, 5, 6, ………] --unendliche Liste
Wie nicht anders zu erwarten, werden Listen rekursiv aus aus Elementen eines
Haskell-Typs a aufgebaut. Dazu braucht man zwei Festlegungen:
•
•
Die leere Liste [] ist eine Liste vom Typ [a] (Induktionsanfang)
Ist x ein Element vom Typ a und xs eine Liste vom Typ [a], dann ist x:xs eine
Liste vom Typ [a]
Durch diese Festlegung (Definition) wird offensichtlich, dass Listen rekursiv
konstruiert werden. Das ist alles ziemlich abstrakt. Daher ein Beispiel:
1, 3 und 5 sind Int-Variablen. Weshalb ist dann nach obiger Definition [1,3,5] eine
Liste (von Ints)?
[1,3,5] = 1 : [3,5]
[3,5] = 3 : [5]
[5] = 5 : []
Somit ist [1,3,5] eine Liste, wenn [3,5] eine Liste ist
[3,5] ist eine Liste, wenn [5] eine Liste ist
[5] ist eine Liste, weil [] eine Liste ist.
[5.1+i*0.1 | i<-[0..9]] = 5.1 + 0*0.1 : [5.1+1*0.01 : […….]]
Und nun liest man die drei Zeilen von unten nach oben und man kommt zum
Schluss, dass auch die Anfangsliste [1,3,5] eine Liste sein muss.
Der Aufbau unserer Liste ist daher
[1,3,5] = 1 : (3 : (5 : [])) oder kurz: [1,3,5] = 1 : 3 : 5 : []
Benutzt man die zweite Schreibweise, so muss einem klar sein, dass eigentlich von
rechts aus geklammert sein müsste und man aus Gründen der Bequemlichkeit diese
Klammern weggelassen hat.
(Mitunter lässt man auch [] noch weg – obwohl die leere Liste ja grundlegend für die
Konstruktion einer Liste ist).
Zeichnerisch veranschaulicht sieht eine Liste daher so aus:
Auch Funktionen, die Listen verarbeiten, sind oft nach dem gleichen rekursiven
Muster aufgebaut. Ein Beispiel ist die unten dargestellte Funktion „anwenden“, die
auf jedes Element einer Liste eine Funktion f anwendet.
(Haskell hat eine solche Funktion natürlich schon implementiert. Sie heißt map).
anwenden:: (a -> b) -> [a] -> [b]
anwenden f [] = []
-- (Induktionsanfang)
14
anwenden f (x:xs) = (f x): anwenden f xs
-- (Induktionsschluss)
In Zeile Induktionsanfang wird die leere Liste durch Musteranpassung abgefangen
(Stoppfall der Rekursion, in Mathematik gerne Induktionsanfang genannt).
In Zeile Induktionsschluss wird die Funktion f auf das erste Element der Liste
angewandt und danach das Ergebnis mit einem rekursiven Aufruf der Restliste
angefügt. Die Funktion anwenden kann auf beliebige Listen losgelassen werden.
Zu beachten ist nur, dass die Parameter zusammenpassen:
Das erste Argument muss eine Funktion sein, die eine Liste vom Typ des zweiten
Parameters (Typ a) erwartet und als Ergebnis eine Liste mit Elementen vom Typ b
zurückliefert. Solche universellen Funktionen nennt man polymorph.
Beispiele:
Main> anwenden sin [0,pi/2,pi]
[0.0,1.0,0.0]
Main> anwenden toUpper "Hallo"
"HALLO"
Ahnen Sie nun, welches Potenzial in dieser Technik stecken?
Angenommen, Sie wollen mal auf die Schnelle das Produkt der natürlichen Zahlen
von 1 bis 200 berechenen. Dann sieht das in Haskell so aus:
Hugs> product [1..200]
788657867364790503552363213932185062295135977687173263294742533244359449963
403342920304284011984623904177212138919638830257642790242637105061926624952
829931113462857270763317237396988943922445621451664240254033291864131227428
294853277524242407573903240321257405579568660226031904170324062351700858796
178922222789623703897374720000000000000000000000000000000000000000000000000
(Nein, Sie müssen das Ergebnis nicht aussprechen......)
Aufgabe 6
Versuchen Sie durch Erfragen des Typs ( z.B. : t sum ) und durch einfaches
Testen, die Wirkungsweise der folgenden Funktionen für Listen zu verstehen:
sum, product, maximum, reverse, length, (++), head, tail, drop, take, init, last
Mit Listen ist es sehr einfach, Funktions-Tabellen zu erstellen.
--Tabelle für die Funktion f von a bis b mit 20 Schritten
tabelle20 ::(Float -> Float) -> Float -> Float -> [Float]
tabelle20 f a b = [f(x)|x<-[a, a+(b-a)/20 .. b]]
Mit dem Ergebnis:
Main> tabelle20 quadrat 0 2
15
[0.0,0.01,0.04,0.09,0.16,0.25,0.36,0.4900001,0.6400001,0.8100002,1.0,1.21,1.44,1.
690001,1.960001,2.250001,2.560001,2.890001,3.240001,3.610001,4.000001]
(Wenn Ihnen die Genaugikeit nicht ausreicht, verwenden Sie Double statt Float)
Um die Anzahl der Schritte variabel zu halten, muss nur wenig geändert werden:
--Tabelle für die Funktion f von a bis b mit n Schritten
tabelle ::(Float -> Float) -> Float -> Float -> Float -> [Float]
tabelle f a b n = [f(a+i*(b-a)/n)|i<-[0..n]]
Schauen Sie sich genau an, wie man erreicht hat, dass n+1 Funktionswerte
(zwischen a und b mit Abstand (b-a)/n) in die Liste geschrieben werden!
Und jetzt ein kleiner Test des neuen Codes:
Eine Tabelle für die Sinusfunktion zwischen 0 und 3 in 100 Schritten ist gewünscht:
Main> tabelle (\x->sin x) 0 3 100
[0.0,0.0299955,0.059964,0.08987855,0.1197122,0.1494381,0.1790296,0.2084599,0
.2377026,0.2667314,0.2955202,0.324043,0.3522742,0.3801884,0.4077604,0.43496
55,0.4617792,0.4881772,0.514136,0.539632,0.5646425,0.5891448,0.6131169,0.63
65372,0.6593847,0.6816388,0.7032794,0.7242872,0.7446431,0.764329,0.7833269,
0.8016199,0.8191916,0.836026,0.852108,0.8674232,0.8819578,0.8956987,0.90863
35,0.9207506,0.9320391,0.9424888,0.9520903,0.960835,0.9687151,0.9757234,0.9
818535,0.9871001,0.9914584,0.9949244,0.997495,0.9991679,0.9999417,0.999815
6,0.9987897,0.996865,0.9940432,0.9903268,0.9857192,0.9802245,0.9738476,0.96
65944,0.9584713,0.9494856,0.9396455,0.9289597,0.917438,0.9050906,0.8919287,
0.8779641,0.8632094,0.8476778,0.8313834,0.8143408,0.7965655,0.7780732,0.758
8807,0.7390053,0.7184649,0.6972778,0.6754631,0.6530407,0.6300306,0.6064535,
0.5823306,0.5576838,0.532535,0.5069069,0.4808225,0.4543056,0.4273798,0.4000
694,0.3723991,0.3443935,0.316078,0.2874781,0.2586192,0.2295279,0.2002299,0.
1707518,0.14112]
Damit kann man natürlich auch noch
mehr anfangen:
Zum Beispiel das Integral einer
stetigen Funktion f näherungsweise
mit der Unter- bzw Untersummenregel
berechenen. (Siehe Bild rechts für die
Obersumme gezeichnet)
Die Fläche unter dem Schaubild von a
bis b bekommt man danach
(ungefähr), wenn man
br*f(a+1*br) + br*f(a+2*br)....+br*f(b)
mit br = (b-a)/n
16
zusammenzählt. Im Bild sind das die fünf gelben Rechtecke. Hier ist das Ergebnis
mit n = 5 natürlich noch sehr schlecht. Wie kann man es besser machen?
Und nun der Programmcode hierzu in Haskell:
--Integral-Näherung
flaeche :: (Double -> Double) -> Double -> Double -> Double -> Double
flaeche f a b n= br*sum [f(a+i*br) | i<-[1..n]]
where br = (b-a) /n
Wem die Schreibweise der Liste mit i<-[1..n] suspekt ist, wählt diese Lösung:
flaeche2 :: (Double -> Double) -> Double -> Double -> Double -> Double
flaeche2 f a b n = br*sum (map f [a,a+br..b])
where br = (b-a) /n
Ein Beispiel dazu (a=0, b=4, n=10000):
Main> flaeche (\x->x^3) 0 4 10000
64.01279
Mit einer Million Abschnitte bekommt man bereits 64.0001280000637.
Der exakte Wert ist 64 (= 1/4* 44 für alle, die die Integralrechnung schon
beherrschen!)
Erinnern Sie sich an das „Sieb des Eratosthenes“? Ein besonders wirkungsvoller
Algorithmus zur Primzahlberechnung mit einem leider sehr hohen Speicherbedarf!
Lesen Sie dazu hier nach: http://de.wikipedia.org/wiki/Sieb_des_Eratosthenes
Und so sieht der „Zweizeiler“ dazu in Haskell aus:
primzahlen = sieb [2..]
where sieb (x:xs) = x:sieb [y |y <- xs , y `mod` x /= 0]
Rekursion und unendliche Liste! So einfach kann ein Programm sein.
Wer will, kann ja mal versuchen, das Programm in Visual C#, Delphi, Java oderPHP
zu programmmieren...
(Achtung: Infix-Operatoren – also solche, die nicht vor den Variablen sondern
zwischen ihnen stehen wie mod, muss man in accents graves einschließen!)
Aufgabe 7
•
Ändern Sie die obige Definition so ab, dass die Primzahlen bis zu einer
bestimmten Zahl ausgegeben werden: primzahlen_bis :: Integral a => a -> [a]
•
Formulieren Sie zunächst eine Definition für einen Primzahltest:
istprim :: Integral a => a -> Bool
(Tipp: Es gibt kein n in [1..x] für das x mod n = 0)
Schreiben Sie damit eine Definition zur Bestimmung von Primzahlzwillingen
bis m.
17
Tupel
Listen sind in Haskell ein ganz wesentliches Element. Erst mit ihnen wird eine
funktionale Programmiersprache erst konkurenzfähig. Dennoch sind auch Listen
nicht immer das passende Mittel. Ein Beispiel: Eine mp3-Datei enthält in der Regel
nicht nur die Musik sondern auch sogenannte Tags in denen Informationen zu dieser
Datei gespeichert sein können.
Ein Tupel, das dem obigen Beispiel gerecht wird, könnte die Form
(String, String, String, String, Int, Int, String) haben. Das erste Tupel wäre dann:
("La Vie en Rose", "Edit Piaf", "Edit Piaf", "World Of Disc 1", 1, 2002)
Der WinHugs- Editor stellt leider nur Tupel mit maximal fünf Elementen dar. Rechnen
kann man aber mit deutlich längeren Tupeln. Eine Funktion, die das zweite Element,
also den Interpret, aus dem Tag ausliest, kann man so definieren:
--zweites Element von sechs
interpret :: (a,b,c,d,e,f) -> b
interpret (a,b,c,d,e,f) = b
Mit dem Aufruf:
Main> interpret ("La Vie en Rose", "Edit Piaf", "Edit Piaf", "World Of Disc 1", 1, 2002)
"Edit Piaf"
Wer seine gesamte Musiksammlung getaggt hat, besitzt also eine Liste aus Tupeln.
Mit der map-und der interpret-Funktion kann man sich dann beispielsweise alle
Interpreten der Sammlung auslesen lassen.
In der Praxis haben die meisten Haskell-Tupel zwei (Paare) oder drei (Trippel)
Elemente. Für Paare gibt es zwei Standardfunktionen:
Main> fst ('a','b')
'a'
Main> snd (3,4)
4
Beachten Sie, dass man diese beiden Funktionen nur auf Tupel mit zwei Elementen
anwenden kann.
Ein Anwendungsbeispiel aus der Mathematik ist das Skalarprodukt im
dreidimensionalen Raum. ( http://delphi.zsg-rottenburg.de/skalarpr.html )
In Haskell lässt sich es so darstellen:
skalarprodukt :: (Float, Float, Float) -> (Float, Float, Float) -> Float
skalarprodukt (x1,x2,x3) (y1,y2,y3) = x1*y1+x2*y2+x3*y3
Mit dem Aufruf:
Main> skalarprodukt (4.5,2.7,6.3) (9.2,4.8,2.6)
70.74
18
Aufgabe 8
•
Definieren Sie eine Funktion tausch2, die die zwei Werte eines Paares
vertauscht.
•
Schreiben Sie eine Funktion abstand2, die den Abstand zweier Punkte
P(x1,y1) und Q(x2,y2) in der Ebene berechnet. ( d = ( x 2 − x1) 2 + ( y 2 − y1) 2 )
Auch im 3-dimensionalen Raum kann man den Abstand zweier Punkte
P(x1,y1,z1) und Q(x2,y2,z2) berechnen. Schreiben Sie hierfür die Funktion
abstand3 . ( d = ( x 2 − x1) 2 + ( y 2 − y1) 2 + ( z 2 − z1) 2 )
•
Es soll der Abstand zweier Städte, deren
Geodaten (Längengrad, Breitengrad) bekannt
sind, errechnet werden. Die Formel:
abstandLB = acos[ sin(breite1)*sin(breite2) +
cos(breite1)*cos(breite2)*cos(laenge2-laenge1) ]
* erdradius
Dabei ist erdradius = 6371 km. acos ist die
Umkehrfunktion des Cosinus.
Beachten Sie: breite1, laenge1 etc. müssen in
Bogenmaß eingegeben werden:
l1 (im Bogenmaß)= laenge1*pi/180 etc.
Zürich hat die Geokoordinaten (Länge,Breite)
(8.54 Grad, 47.38 Grad).
Los Angeles liegt auf (-118.39 Grad, 33.94 Grad). Wieviel km beträgt der
kürzeste Abstand?
Mit GoogleEarth können Sie sich übrigens die Geodaten jedes beliebigen
Ortes beschaffen. Da diese aber in Grad, Minuten und Sekunden angegeben
werden, müssen sie erst in Dezimalzahlen umgewandelt werden.
Beispiel: 47 0 40’ 55,44’’ = 47 + 40/60 + 55,44/3600 = 47,6820666 0
Hier ein einfaches Beispiel, wie man mit einer Liste von Tupeln umgehen kann:
Sind mehrere nummerierte Punkte einer Ebene gegeben, so kann man einen
Streckenzug von Punkt 1 zu Punkt 2 etc. und schließlich bis zum Endpunkt festlegen.
19
Die Liste könnte so aussehen:
punktL = [(1.0,1.2), (0.3, -2.4), (-3.9, 4.7), (6.7,2.9)]
Eine Liste ( laengenL2 )mit den Abständen zwischen den einzelnen Punkten wäre
hilfreich, die Funktion gesamtlaenge2 zu definieren. Für das obige Beispiel:
laengenL = [3.66742416417845, 8.24924238945614, 10.7517440445725]
und hat damit ein Element weniger als die ursprüngliche Liste aus Paaren.
laengenL2 :: [(Double,Double)] -> [Double]
laengenL2 [ ] = [ ]
laengenL2 (x : [ ]) = [ ] -- nur ein Punkt ergibt keine Länge!
laengenL2 (p1 : p2 : restpunkte) = (abstand2 p1 p2) : (laengenL2(p2 : restpunkte))
Ein kurzer Test der Hilfsfunktion zeigt, dass wir auf dem richtigen Weg sind. Die
Streckenlängen zwischen den Punkten müssen nun noch aufaddiert werden. Zwar
gibt es mit sum eine passende Listenfunktion in Haskell – wir werden, der Übung
halber diese Funktion selbst schreiben:
summe :: Num a => [a] -> a
summe [ ] = 0
summe (x : xs) = x + (summe xs)
Aufgabe 9
•
•
Definieren Sie mit den neuen Hilfsfunktionen die gesuchte Funktion
gesamtlaenge2. Zeigen Sie, dass die Gesamtlänge im obigen Beispiel
22.6684105982071 beträgt.
Definieren Sie eine Funktion gesamtlaenge3, die die Länge eines
Streckenzuges im dreidimensionalen Raum berechnet. Verwenden Sie als
Beispiel die Punkteliste: [(2.3,7.4,9.2),(3.4,6.9,-3.2),(-3.0,4.2,-2.8),(6.7,3.1,1.2)]
Currying und uncyrrying
Bisher waren unsere Funktionen so aufgebaut, dass man einen Wert nach dem
anderen ohne Klammer hinter die Funktion schrieb. Beispiel: plus 5 7 . Dies nennt
man nach Haskell Curry currying.
Das hat, wie wir später noch genauer sehen werden, den Vorteil, dass man auch
partielle Funktionen erzeugen kann. Etwas plus5 = plus 5.
Andererseits ist es mit Hilfe der Tupel jetzt auch möglich, der Funktion alle Werte auf
einmal zu übergeben (uncurrying). In unserem Beipiel könnte man ein „Paar-Plus“
so definieren:
plusP :: (Float, Float) -> Float
plusP (x,y) = x + y
Zwischen diesen beiden Funktionen kann man beliebig hin- und her springen:
curry addiereP 3 5
Oder auch anders herum:
uncurry addiere (3,5)
Testen Sie das Currying und das Uncurrying!
20
Funktionen für Listen
Sobald man einige Zeit mit Listen gearbeitet hat, merkt man, dass es praktisch wäre,
wenn man Funktionen zur Verfügung hat, die etwas „Listen-typisches“ können, wie
zusammenfügen, ordnen, einfügen oder entfernen. Beginnen wir daher mit einer
Aufgabe
Aufgabe 9
•
Schreibe einen Programmcode für eine Funktion:
entfernenInt :: Integer -> [Integer] -> [Integer]
Beispiel :
Main> entfernenInt 3[1,2,3,4,5]
[1,2,4,5]
•
Schreibe den obigen Programmcode um,so dass entferne ein überladene
Funktion wird. Beachten Sie, dass ein Vergleich für die Elemente
vorgenommen werden muss. Die Klasse, aus der die Elemente genommen
werden können, ist also mindestens Eq.
•
Erzeuge eine Funktion zusammen, die zwei gleichartige Listen zu einer Liste
zusammenfügt.
Beispiel:
Main> zusammen [2,4,6] [1,3,5]
[2,4,6,1,3,5]
Die wichtigsten Funktionen, die auf Listen wirken, sind polymorph oder wenigstens
überladen. Im ersten Fall kann man sie für alle Listen verwenden, im zweiten Fall auf
eine ganze Gruppe, wie zum Beispiel Listen, die aus Zahlen bestehen (Num).
Die oben selbst gebastelte Funktion zusammen, ist polymorph. Sie ist in Haskell
bereits vordefiniert durch ++
Main> [2,4,6] ++ [1,3,5]
[2,4,6,1,3,5]
Etwas anderes ist es, zwei bereits geordnete Listen zu einer geordenten Liste
zusammenzufügen. Machen Sie sich an einem einfachen Beispiel, wie etwa [1, 3, 5]
und [2, 4, 6] klar, dass folgender Programmcode diesen Zweck erfüllt:
merge [] ys = ys
merge (x:xs)[] = x:xs
--Induktionsanfang
merge (x:xs) (y:ys)
| x <= y = x:merge xs (y:ys)
| x > y = y:merge (x:xs) ys
Main> merge [1,3,5] [2,4,6]
[1,2,3,4,5,6]
Erstes Zwischenergebnis: 1 : merge [3, 5] [2, 4, 6]
Zweites Zwischenergebnis: 1 : 2 : merge [3, 5] [4, 6]
21
Wie geht es weiter?
Problem: Wie kann man eine Funktion merge_ordnung auf zwei ungeordneten
Listen definieren, die die beiden in eine geordnete Liste zusammenfügt? Also z.B.:
merge_ordnung [3, 2] [9, 1] = [1, 2, 3, 9]
Zunächst ist klar, dass man die beiden Listen nur einzeln ordnen muss. Denn dann
kann man die obige Funktion merge darauf anwenden.
Wir brauchen also eine Funktion sortiere :: Ord a => [a] -> [a], also:
sortiere [4, 1, 6] = [1, 4, 6]
sortiere
Hierfür werden wir das sogenannte Sortierverfahren insertion-sort verwenden. Die
Idee ist sehr einfach:
• Zunächst benötigt man eine passend_einsetzen – Funktion. Sie soll ein
einzelnes Element korrekt in eine schon geordnete Liste einfügen.
Zum Beispiel: passend_einsetzen 4 [2, 6] = [2, 4, 6]
• Dann setzt man jedes einzelne Element der ungeordneten Liste durch die
Funktion nach_rechts in eine neue, diesesmal aber automatisch geordnete
Liste ein. Die neue Liste ist zunächst leer.
Ein Beispiel:
sortiere [4, 1, 6] = nach_rechts [4, 1, 6] [ ]
= nach_rechts [1, 6] (passend_einsetzen 4 [ ])
= nach_rechts [1, 6] [4] = nach_rechts [6] (passend_einsetzen 1 [4]) = ?
Zunächst also erstellen wir eine Funktion für passend_einsetzen. Polymorph können
wir die Funktion nicht gestalten, da Relationen, wie „größer“ oder „kleiner“ möglich
sein müssen. Die größtmögliche passende Klasse wäre Ord . Eingabe sind ein OrdElement und eine Ord-Liste. Ausgabe ebenfalls eine Ord-Liste:
passend_einsetzen :: Ord a => a -> [a] -> [a]
Nun noch die Festlegung, wie passend_einsetzen auf eine leere Liste und wie auf
eine gefüllte wirken muss:
passend_einsetzen y [ ] = [y]
passend_einsetzen y (x:xs)
-- x : xs ist schon geordnet
| y<x
= y:x:xs
-- y ist kleinstes Element
| otherwise = x: (passend_einsetzen y xs)
-- x ist kleinstes Element
Es fehlt noch die Festlegung für nach_rechts. Zwei Listen werden eingegeben und
eine Liste wird ausgegeben:
nach_rechts :: Ord a => [a] -> [a] -> [a]
Am Ende ist die Rekursion, wenn es aus der linken Liste nichts mehr in die rechte
Liste (=gliste für „geordnete Liste“) eingefügt werden kann, weil erstere leer ist.
nach_rechts [ ] gliste = gliste
Ist die linke Liste nicht leer, so wird ihr erstes Element in die geordnete_liste passend
eingesetzt:
22
nach_rechts (x:xs) gliste = nach_rechts xs (passen_einsetzen x gliste)
Aufgabe 9
Versuchen Sie mit Hilfe des obigen Beispiels die Rekursion zu verstehen.
Testen Sie nach_rechts z.B. durch
Main> nach_rechts [3,6,1] [2,5]
[1,2,3,5,6]
Versuchen Sie nun die Funktion sortiere mit Hilfe von nach_rechts zu erstellen!
(Tipp: Bringen Sie die ungeordnete Liste nach_rechts in eine zunächst leere Liste!)
merge_ordnung
Das Ziel war, zwei ungeordnete Listen durch eine Funktion namens merge_ordnung
in einer geordneten Liste zu vereinigen.
Da wir bereits die Funktion nach_rechts haben, die lediglich davon ausgeht, dass die
rechte Liste bereits geordnet ist, müssen wir daher nur mit sortiere dafür sorgen,
dass die rechte Liste auch geordnet ist:
--Zwei ungeordnete Listen in einer geordneten Liste vereinigen
merge_ordnung :: Ord a => [a] -> [a] -> [a]
merge_ordnung liste1 liste2 = nach_rechts liste1 (sortiere liste2)
elementnummer
Die Listen in Haskell sind mit Null beginnend indiziert. Das bedeutet, dass man sich
zum Beispiel das dritte Element einer Liste ausgeben lassen kann:
Main> [2,3,4,5,6] !! 2
4
Zur Übung schreiben wir einen solchen Indexzugriff selbst. Der Aufruf soll so
aussehen: elementnummer [2,4,6,8] 1 (= 4)
So kann man ihn erzeugen:
--indexzugriff eigen
elementnummer
:: [a]->Int->a
elementnummer (x:xs) 0 = x
elementnummer (x:xs) (n+1) = elementnummer xs n
23
map-, filter- und fold-Studien
Die map-Funktion ist vermutlich die wichtigste aller Listen-Funktionen. Wir haben sie
oben bereits selbst als „anwenden“ – Funktion definiert. Zur Erinnerung:
anwenden:: (a -> b) -> [a] -> [b]
anwenden f [] = []
-- (Induktionsanfang)
anwenden f (x:xs) = (f x): anwenden f xs
-- (Induktionsschluss)
Im Folgenden können Sie also immer wenn die Funktion map verwendet wird,
ebenso gut die Funktion anwenden nutzen, sofern Sie sie importiert haben.
Als weiteres Beispiel für die map- Funktion werden wir die Caesar-Verschlüsselung
programmieren. Hier wird jeder Buchstabe um einen bestimmten Wert n im
Alphabeth verschoben. Für n = 3 würde beispielsweise aus C -> F und aus Z -> C.
Wir wollen, wie in der Kryptologie üblich, nur Großbuchstaben ohne Umlaute
verwenden.
Diese Art der Verschlüsselung ist zugegebenermaßen nicht ernst zu nehmen. Hat
man die Technik im Umgang mit Character aber einmal verstanden, so sind
anspruchsvollere Verschlüsselungen kein Problem mehr.
WinHugs muss den Umgang mit ASCII-Tabellen erst beigebracht werden. Dazu
benutzen Sie das Modul Char.hs im Verzeichnis
C:\Programme\WinHugs\packages\hugsbase\Hugs
Am besten kopieren Sie alles aus dieser Datei bis auf die zu ladenden Module, die ja
schon geladen sind, in Ihre neue Datei namens caeser.hs.
Nach dem Laden zeigt Ihnen der Befehl
Hugs.Char> ord 'A'
65
dass der Buchstabe A durch 65 im ASCII-Code festgelegt ist.
Will man zu einem ASCII-Code das zugehörige Zeichen wissen, so schreibt man:
Hugs.Char> chr 90
'Z'
Will man die Großbuchstaben von A bis Z bei Null beginnend durchnummeriert
haben, so bietet sich folgende Funktion an:
stelle :: Char -> Int
stelle zeichen = (ord zeichen) -65
Und die Umkehrung:
buchstabe :: Int -> Char
buchstabe stelle = chr ( stelle + 65)
Jetzt kann man durch Eingabe der Stelle des Zeichens und des Schlüssels n die
neue Position des verschlüsselten Buchstabens errechen:
verschiebe :: Int -> Int -> Int
verschiebe n stelle = mod (stelle + n) 26
24
Nimmt man alles zusammen, so lässt sich eine Funktion caesar auf Character
definieren:
caesar :: Int -> Char -> Char
caesar n zeichen = buchstabe (verschiebe n (stelle zeichen))
In der Mathematik würde die caesar n – Funktion als Verkettung geschrieben:
caesar n (zeichen) = (buchstabe(verschiebe n (stelle(zeichen)))
= (buchstabe o verschiebe n o stelle) (zeichen)
Und genau so wird dies auch in Haskell gemacht, nur der Verknüpfungszeichen o
wird als einfacher Punkt geschrieben:
caesarV :: Int -> Char -> Char
caesarV n zeichen = (buchstabe . verschiebe n . stelle) zeichen
(Das große V im Namen soll auf Verknüpfung hindeuten.)
Und nun kommt map ins Spiel:
Main> map (caesar 4) "HALLOALLEMITEINANDER"
"LEPPSEPPIQMXIMRERHIV"
Der String „HALLO….“ ist eine Liste aus Character. Also kann man mittels map die
(caeser 4) –Funktion auf alle Character wirken lassen. Bequemer geht es nicht!
Aufgabe 10
Definieren Sie eine Funktion caesarString :: Int -> String -> String, die unter
Verwendung eines Schlüssels n einen vorgegebenen String aus Großbuchstaben
verschlüsselt.
Wie kann man diese Funktion für die Entschlüsselung eines Textes verwenden?
Aufgabe 11
Die Funktion caesarString aus Aufgabe 10 wurde durch die Verknüpfung der drei
Funktionen buchstabe, verschiebe n und stelle erzeugt. Natürlich kann man eine
derartige Funktion auch in einem Zug definieren. So zum Beispiel:
caesarText :: Int -> String -> String
caesarText _ [] = []
caesarText n (x:xs) = verschluesselt : caesarText n xs
where
verschluesselt = chr(65 + mod (ord x - 65 + n) 26)
Versuchen Sie die Wirkungsweise der Funktion caeserText nachzuvollziehen und
testen Sie die Definition.
Wie kann man den verschlüsselten Text wieder entschlüsseln? (Tipp: Man denkt
zunächst daran, den Wert der Verschiebung durch Subtraktion wieder rückgängig zu
machen. Das würde aber bedeuten, dass wir eine neue Funktion schreiben müssten.
Man kann aber durch eine geeignete Addition ebenfalls erreichen, dass die
Verschiebung wieder rückgängig gemacht wird…)
25
Wie Sie wissen, ist die Cäsar-Verschlüsselung sehr leicht zu entschlüsseln. Es gibt ja
nur 26 Möglichkeiten der Verschiebung. Sehr viel schwerer macht man es dem
Angreifer, wenn die Buchstaben der Reihe nach mit verschiedenen Werten
verschoben wird. Ein Beispiel: Das Wort „HALLO“ könnte immer abwechselnd um 1
und 2 verschoben werden.
Man könnte sich daher eine Funktion caesarVar vorstellen, die man so aufruft:
caesarVar [1,2] „HALLO“
"ICMNP"
Nach einigen Überlegungen erkennt man jedoch, dass noch ein „Akkumulator“ mit
auf den Weg gegeben werden muss, der festhält, wo man gerade in der Liste sich
befindet bzw. bei welchem Wert man starten soll. In der Regel dürfte dies Null sein,
so dass der Aufruf dann so aussehen würde: caesarVar 0 [1,2] „HALLO“
Die Liste [1,2] bezeichnet man übrigens als „Schlüssel“, da ohne sie kein „öffnen“ des
Geheimtextes möglich ist. (Zugegeben: Bei einem so kurzen Schlüssel kommt man
vermutlich auch mit „Gewalt“ – brutal force – an den Inhalt!)
Hier ein Vorschlag für eine solche Funktion:
caesarVar :: Int -> [Int] -> String -> String
caesarVar n schluessel [ ] = [ ]
caesarVar n schluessel (x:xs)
| (n+1) < length(schluessel) = caesar m x : caesarVar (n+1) schluessel xs
| (n+1) >= length(schluessel) = caesar m x : caesarVar 0 schluessel xs
where m = elementnummer schluessel n
Aufgabe 12
Die folgenden beiden Aufgaben sind relativ einfach zu lösen und zeigen dabei
eindrucksvoll, welche Möglichkeiten man mit der Funktion map hat:
• Ein Internetshop verkauft verschiedene Artikel, mit Netto-Preisen von 1€, 2€,
3€,…,2000€ - also immer Cent-freie Beträge. Nun muss der Kunde aber noch
19% Mehrwertsteuer bezahlen, bekommt aber auf den Gesamtbetrag bei
Sofort-Zahlung 3% Skonto.
Definieren Sie eine dazu passende Funktion endpreis, mit deren Hilfe dann
endpreisListe erzeugt werden kann. Anwendung:
endpreisListe [1..2000] soll dann alle möglichen Endpreise erzeugen.
•
Viel besser wäre es, wenn aus der Netto-Preisliste eine Paar-Liste aus NettoPreis und Endpreis erzeugt würde. Definieren Sie eine passende Funktion
listeInPreisEndpreisListe :: [Float] -> [(Float, Float)]
Tipp: Erzeugen Sie zunächste eine Funktion listeInPaarListe :: [a] -> [(a,a)],
dann preisEndpreis :: (Float, Float) -> (Float, Float) und schließlich
preisEndpreisListe :: [(Float, Float)] -> [(Float, Float)]. Anwendungsbeispiel:
Main> listeInPreisEndpreisListe [1..10]
[(1.0,1.1543),(2.0,2.3086),(3.0,3.4629),(4.0,4.6172),(5.0,5.7715),(6.0,6.9258),(
7.0,8.0801),(8.0,9.234401),(9.0,10.3887),(10.0,11.543)]
26
Eine Filterfunktion, die rekursiv bestimmte Elemente einer Liste auswählt, ist mit
filter in Haskell bereits fest vorgegeben:
filter :: (a -> Bool) -> [a] -> [a]
Man benötigt offensichtlich eine Bedingung für a und eine Liste. Beispiel zunächst für
eine Bedingung:
istnegativ :: (Ord a, Num a) => a -> Bool
istnegativ zahl = (zahl < 0)
Es mag Sie verwundern, dass man zwei Klassen angeben muss, um eine solche Bedingung für alle
Zahlen zu definieren. Num reicht nicht, weil in Num auch komplexe Zahlen definiert sind. Für diese
gibt es kein „kleiner“. Ord reicht nicht, weil der Vergleich mit einer Zahl (0) erfolgt. Natürlich könnte
man sich das Leben auch einfach machen und (mit Recht) darauf vertrauen, dass bei der Definition
istnegativ :: Float -> Bool auch alles gut geht, weil eine Integer (ohne Dezimalpunkt) in Haskell als
Float durchgeht…
Damit kann man eine Filterfunktion für Listen aus Zahlen definieren:
nurNegative :: (Ord a, Num a) => [a] -> [a]
nurNegative liste = filter istnegativ liste
Beispiel:
Main> nurNegative [-20..20]
[-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1]
Soweit die Vorübung. Die Filterfunktion soll, um die Verschlüsselung von Text später
etwas komfortabler zu gestalten, alle Blanks aus einem Text entfernen.
In einem zweiten und dritten Schritt werden später dann noch alle Buchstaben in Großbuchstaben
verwandelt und danach die Umlaute ersetzt (Ö = OE etc.), so dass man einen gewöhnlich
geschriebenen Text in die für die Verschlüsselung korrekte Form bringen kann. Zifffern und
Sonderzeichen werden bei dieser Form der Verschlüsselung jedoch nicht berücksichtigt!
Die Bedingung:
istNichtBlank :: Char -> Bool
istNichtBlank zeichen = (zeichen /= ' ')
Die Funktion mit Anwendung:
nichtBlank :: String -> String
nichtBlank text = filter istNichtBlank text
Main> nichtBlank "Hallo alle miteinander!"
"Halloallemiteinander!"
Aufgabe 13
Um einen beliebigen Text für die Standard-Verschlüsselung anzupassen, müssen
nicht nur die Blanks entfernt werden. Danach sollte man alle Umlauteund ß
umschreiben (Ö = OE etc.) und schließlich die Kleinbuchstaben in Großbuchstaben
verwandeln. Damit keine Sonderzeichen oder Ziffern übrig bleiben, muss am Ende
noch nach Großbuchstaben gefiltert werden. (“ dürfen im Text dennoch nicht
verwendet werden!) Ein solche zusammengesetzte Funktion könnte so aussehen:
textBereinigen :: String -> String
textBereinigen text = filter istGrossbuchstabe ((inGrossbuchstaben .
umlauteUndScharfSWeg . nichtBlank) text)
Schreiben Sie die passenden Teilfunktionen und die Bedingung istGrossbuchstabe
27
Ausgesprochen praktisch sind auch die fold-Funktionen foldl und foldr.
Eine Anfrage mit :t gibt Auskunft über den Typ der Funktion:
Main> :t foldl
foldl :: (a -> b -> a) -> a -> [b] -> a
(Gilt auch für foldr)
fold erwartet als Eingabe eine Relation, einen beliebigen Wert und eine Liste.
Zwei einfache Beispiele (Klammern dienen hier Demonstration der Reihenfolge):
•
foldl (+) 0 [1,2,3] = fold (+) (0+1) [2,3] = foldl (+) ((0+1)+2) [3] =
foldl (+) (((0+1)+2)+3) [ ] = (((0+1)+2)+3) = 6
•
foldr (*) 1 [4,5,6] = foldr (*) (1*6) [4,5] = foldr (*) ((1*6)*5) [4] =
foldr (*) (((1*6)*5)*4) [ ] = (((1*6)*5)*4) = 120
Die Anfangswerte 0 bzw. 1 dienen hier als Akkumulator. Meist gibt es, abhängig von
der Relation, einen Standard-Akkumulator. Mehr dazu unter Partielle Funktionen.
Mit diesen Funktionen und mit der bereits erstellten Funktion passend_einsetzen
lässt sich eine sehr einfache Sortierfunktion beispielsweise für Float-Werte
definieren:
floatSort :: [Float] -> [Float]
floatSort xs = foldr passend_einsetzen [ ] xs
Dass foldl und foldr verschiedene Ergebnisse haben können, erkennt man, wenn
man als Relation die üblichen (+) bzw. (*)-Beispiele verlässt.
Aufgabe 14
•
Untersuchen Sie, welche Definition und Wirkung die Relation (:) hat.
•
Mit flip werden die Eingabeparameter vertauscht: (-) 3 4 = -1, während
flip (-) 3 4 = 1. Welche Definition und Wirkung hat die Relation (flip (:) )?
•
Definieren Sie nun ein Funktion invertiere :: [a] -> [a] , die eine Liste
umdreht: Aus [1,2,3] wird [3,2,1]. Tipp: Verwenden Sie die Funktion (flip (:) )
als Relation für foldl. Welchen „neutralen“ Parameter muss man übergeben?
Um zum Beispiel das Maximum einer Liste zu suchen, kann man die Funktion foldl1
verwenden. Sie ist definiert durch: foldl1 f (x:xs) = foldl f x xs
Das bedeutet, dass foldl1 nicht auf leere Listen wirken kann, denn als zu
übergebende Parameter wird einfach das erste Listenelement verwendet. Als
Relation bietet sich max :: Int -> Int -> Int an:
maximum :: Ord a => [a] -> a
maximum liste = foldl1 max liste
Wenn Sie maximum auf eine leere Liste anwenden, bekommen Sie daher eine
Fehlermeldung.
28
Partielle Funktionen
Wenn man der Bequemlichkeit halber immer mit Schlüssel n = 5 arbeiten will, so
lässt sich eine angepasste Funktion definieren:
caesarString5 :: String -> String
caesarString5 = caesarString 5
caesarString erwartet zwei Eingaben, caesarString5 nur eine! Diese Art der
Auswertung der Funktion caesarString nennt man partiell.
Aus einer Funktion
f :: Int -> String -> String entsteht die Funktion
f n :: String -> String , die dann auch kurz
h :: String -> String genannt werden kann.
Partielle Funktionen müssen daher die ersten Eingaben (links) der ursprünglichen
Funktion bekannt sein. Dadurch entsteht eine neue Funktion, die auf die Eingabe der
restlichen Parameter (rechts) wartet.
caesarVar in partielle Funktion verwandeln
Die obige Funktion caesarVar benötigt immer noch die Eingabe des Akkumulators.
Dieser ist aber in aller Regel Null. Deshalb liegt es nahe, die partielle Funktion
caesarVariabel :: [Int] -> String -> String
caesarVariabel = caesarVar 0
zu definieren.
Aufruf und Ergebnis sehen dann so aus:
Main> caesarVariabel [3,7,1,9,11,22,4,15,21,5]
"WERDENSCHLUESSELNICHTKENNTHATWENIGCHANCEN"
"ZLSMPJWRCQXLTBPHRXXMWRFWYPLPOBHUJPNDECXJQ"
Aufgabe 15
Ein Schlüssel der Länge 10 angewandt auf einen Text der Länge 41 – weshalb ist
das ohne Kenntnis des Schlüssels kaum zu dechiffrieren?
Weshalb machen im Schlüssel keine Zahlen oberhalb 26 einen Sinn?
Im Schlüssel dürfen Zahlen auch mehrfach vorkommen. Ist in diesem Fall die
Entschlüsselung einfacher?
Was nutzt die beste Verschlüsselung, wenn man den Geheimtext nicht mehr
entschlüsseln kann? Wie wir oben (hoffentlich) eingesehen haben, muss man einen
Buchstaben, der beispielsweise mit 5 verschoben wurde, ein zweites Mal mit (26 – 5)
= 21 verschieben und man kommt beim alten Buchstaben raus.
Das bedeutet, dass man von jede einzelne Ziffer des Schlüssels 26 abziehen muss
und dann diesen neuen Schlüssel der Funktion caesarVariabel mitsamt dem
Geheimtext vorsetzten muss.
Definieren wir hierzu eine Hilfsfunktion
29
gegenschluessel :: [Int] -> [Int]
gegenschluessel schluessel = map (\x -> 26 - x) schluessel
Dann kann man die Entschlüsselungsfunktion so definieren:
caesarVariabelEntschluesseln schluessel geheimtext
= caesarVariabel (gegenschluessel schluessel) geheimtext
Das Ganze in der Anwendung:
Main> caesarVariabelEntschluesseln
[3,7,1,9,11,22,4,15,21,5]"ZLSMPJWRCQXLTBPHRXXMWRFWYPLPOBHUJPNDEC
XJQ"
"WERDENSCHLUESSELNICHTKENNTHATWENIGCHANCEN"
Mit der Funktion caesarVariabel und ihrer Umkehrfunktion haben wir ein einfaches
aber wirkungsvolles Mittel zur sicheren Verschlüsselung konstruiert. Wirklich sicher
darf sich aber nur derjenige fühlen, der einen hinreichend langen Schlüssel
verwendet und – das ist der Haken an der Sache – diesen Schlüssel sicher dem
Empfänger übergibt.
Man nennt diese Art der Verschlüsselung übrigens polyalphabetisch (im Gegensatz
zur Cäsarverschlüsselung, die monoalphabetisch genannt wird). Vigenère (16.Jhrt)
hat diese Idee als Erster dokumentiert. Hier finden Sie einige Informationen:
http://de.wikipedia.org/wiki/Polyalphabetische_Substitution
Damals wie heute verwendet man als Schlüssel keine Ziffernfolge, wie wir das tun,
sondern man verwendet Buchstaben. Der Schlüssel ABBA wäre in unserer
Schreibweise dann [1,2,2,1], also A bedeutet Verschiebung um 1, …, Z bedeutet
Verschiebung um 26. Passen wir unsere Funktionen dementsprechend an. Das
Einzige was wir dazu benötigen ist eine Funktion verwandle , die aus einem StringSchlüssel einen Ziffernschlüssel macht:
verwandle :: String -> [Int]
-- verwandelt String in [Int] mit A = 1, …
verwandle text = map (\x -> ord x - 64) text
Die Funktion vigenere, die statt einer Ziffernfolge einen String-Schlüssel erwartet,
kann dann so geschrieben werden:
vigenere :: String -> String -> String
vigenere schluesselwort text = caesarVariabel (verwandle schluesselwort) text
Aufgabe 16
•
•
Definieren Sie die zugehörige Entschlüsselungsfunktion
vigenereEntschluesseln und testen Sie Verschlüsselung und
Entschlüsselung an einigen Beispielen.
Wie lang sollte ein sinnvoller Schlüssel maximal sein?
Kombinieren Sie die Funktion vigenere mit der oben behandelten Funktion
textBereinigen, sodass auch beliebige Texte (aber ohne “) eingegeben werden
können.
30
sortiere als partielle Funktion betrachten
Zur Übung werden wir die obige sortiere –Funktion als partielle Funktion einer
allgemeineren Funktion betrachten. Denn es gibt ja ganz offensichtlich viele
Möglichkeiten, eine Liste zu sortieren. Bisher ist sortiere nur auf Listen der
Elementen-Klasse Ord (Num und Char) erklärt und es wird von klein nach groß
geordnet. Beispiel:
Main> sortiere "Unordnung"
"Udgnnnoru"
(Beachte: ord ’U’ = 85 aber ord ’u’ = 117)
Wir wollen eine Funktion sortiereA :: (a -> a -> Bool) -> [a] -> [a] definieren, die als
erste Eingabe eine Relation (a -> a -> Bool) auf a erwartet. Hierbei sind der
Phantasie keine Grenzen gesetzt. Ein einfaches Beispiel wäre die Relation (>), die
zu einer absteigenden Sortierung führen würde.
Wenn die Relation gar auf Paaren wirkt, ergeben sich noch viel mehr Möglichkeiten.
Ein Beispiel: Die Paare (Hans, Müller), (Gustav, Gans), (Marion, Fink), … werden im
Allgemeinen nach dem Nachnamen, also hier dem zweiten Eintrag des Paares,
geordnet.
Zur Erinnerung die bisherige sortieren – Funktion mit ihren Hilfsfunktionen:
-- Element in geordnete Liste einordnen
passend_einsetzen :: Ord a => a -> [a] -> [a]
passend_einsetzen y [ ] = [y]
passend_einsetzen y (x:xs)
| y<x
= y:(x:xs)
| otherwise = x: (passend_einsetzen y xs)
-- x : xs ist schon geordnet
-- y ist kleinstes Element
-- x ist kleinstes Element
--Hilfsfunktion nach_rechts
nach_rechts :: Ord a => [a] -> [a] -> [a]
nach_rechts [ ] gliste = gliste
nach_rechts (x:xs) gliste = nach_rechts xs (passend_einsetzen x gliste)
--insertion sort
sortiere :: Ord a => [a] -> [a]
sortiere liste = nach_rechts liste [ ]
Schreiben wir zunächst die erste Hilfsfunktion um:
-- Element in geordnete Liste einordnen allgemeine Relation
passend_einsetzenA :: (a -> a -> Bool) -> a -> [a] -> [a]
passend_einsetzenA relation y [ ] = [y]
passend_einsetzenA relation y (x:xs)
-- x : xs ist schon geordnet
| relation y x
= y:(x:xs)
-- y ist nach vorgebener Relation vorderes Element
| otherwise = x: (passend_einsetzenA relation y xs)
-- x vorderes Element
Kurzer Test:
Main> passend_einsetzenA (>) 5 [7,6,1]
[7,6,5,1]
31
Fehlt noch die verallgemeinerte Hilfsfunktion
--Hilfsfunktion nach_rechtsA
nach_rechtsA :: (a -> a -> Bool) -> [a] -> [a] -> [a]
nach_rechtsA relation [ ] gliste = gliste
nach_rechtsA relation (x:xs) gliste = nach_rechtsA relation xs (passend_einsetzenA
relation x gliste)
Schlussendlich noch die verallgemeinerte Sortierfunktion:
sortiereA :: (a -> a -> Bool) -> [a] -> [a]
sortiereA relation liste = nach_rechtsA relation liste [ ]
Erneuter Test:
Main> sortiereA (>) [4,5,6,1,44,42,82,34]
[82,44,42,34,6,5,4,1]
Die Relation „ist kleiner“ (>) wäre ganz sicher kein Grund, eine verallgemeinerte
Sortierfunktion zu schreiben. Der eigentliche Grund sind, wie schon erwähnt,
Relationen, die nicht vordefiniert sind wie „ist kleiner“ zwischen zwei Zahlen.
Betrachten Sie hierzu eine Liste von 3-er-Tupeln.
Erster Eintrag: Vorname, zweiter Eintrag: Nachname, dritter Eintrag: Alter. Beispiel:
[(„Anton“,“Maier“,53), („Silke“, „Born“, 21), („Anja“, „Dom“,44), („Michael“, „Hank“, 72)]
Wer diese Tupel ordnen soll, fragt doch zunächst, nach welchem Kriterium zu ordnen
ist, mit anderen Worten, welche Relation zu verwenden ist. Und genau das ist ein
Fall für unsere verallgemeinerte Sortierfunktion.
Wir wollen die Tupel nach Alter ordnen. Also brauchen wir erst eine passende
Relation:
istjuenger :: Ord c => (a,b,c) -> (a,b,c) -> Bool
istjuenger (x1,y1,z1) (x2,y2,z2) = z1<z2
Die passende Sortierfunktion:
sortiereNachAlter :: Ord c => [(a,b,c)] -> [(a,b,c)]
sortiereNachAlter = sortiereA istjuenger
Mit fold lassen sich viele Operationen auf Listen anwenden. Ein Beispiel:
Sie wollen die logische Operation or auf eine boolsche Liste [true, false,true,false]
anwenden. Wenn wir die Liste von links nach rechts lesen wollen, dann wäre dies
eine Lösung, die foldl als partielle Funktion nutzt:
or = foldl (||) False
Aufgabe 17
•
•
Schreiben Sie nun eine Funktion für die obigen Tupel, die nach Nachnamen
sortiert.
Tipp: Zuerst die Relation kleinerNachname definieren!
Drücken Sie die logische Operationen and mit foldl aus. Ergibt die
Kombination mit foldr das gleiche Ergebnis?
32
In Aufgabe 4 haben wir uns mit der Fibonacci-Folge beschäftigt. Es war zwar nicht
weiter schwierig, das Problem rekursiv zu lösen, - die Ausführungsgeschwindigkeit
war allerdings mehr als bescheiden. Mit Listen geht es viel, viel schneller:
fibschnell :: [Integer]
fibschnell = 0 : 1 : (zipWith (+) fibschnell (tail fibschnell))
Wie Sie oben schon selbst herausgefunden haben entfernt tail das erste Element
einer Liste. Neu ist für uns zipWith: Hier werden zwei Listen elementweise mit einer
bestimmten Funktion ( hier (+) ) verknüpft. Zum Beispiel die Verknüpfung mit (*):
Main> zipWith (*)[1, 4, 6] [7, 8, 9]
[7,32,54]
Der obige Programmcode für die FibonacciFolge ist eine „Konstruktionsvorschrift“:
0 : 1 : (zipWith (+) [0,1,f1,f2,f3..] [1,f1,f2,f3..])
Man erkennt sofort, dass f1 =1 sein muss.
Daraus ergibt sich dann f2 etc.
Hier noch ein gut gemeinter Rat: Positionieren
Sie die Maus direkt über dem kleinen Quadrat
für „Stop program execution“ bevor Sie die
Enter-Taste drücken.
Denn es darf nicht viel Zeit vergehen zwischen
Programmstart und Stopp. Sehen Sie selbst,
weshalb das so ist!
Bei fast gleichzeitigem Drücken von Keybordund Maustaste ist die letzte Fibonacci-Zahl vor
dem „Interrupt“:
255239891487955204763028765174429290225574698227916993274034384901108067924
350903389446104490355598143955494037924409485928126310226314831809427765737
181917594632
Sind Sie schneller?
(Es ist schon eine beachtliche Leistung, wenn man die ersten Fibonacci-Zahlen noch
auf der Liste hat. Denn wegen der maximal zulässigen Anzahl von Zeichen auf dem
Editor, wird immer wieder von vorn her gelöscht. Nach bereits recht kurzer Zeit stoppt
der Prozess, denn dann ist der „Papierkorb“ voll! Die ganze Liste – fünfzehn eng
beschriebene Seiten- liegt im Tauschverzeichnis: FibonacciZahlen.doc )
Das obige Konstrukt nennt man übrigens eine unendliche Liste! „Unendlich“ ist sie
natürlich nur theoretisch ...
Auch das ist eine unendliche Liste:
quadrate = [n*n | n <- [0..]]
Testen Sie diese und andere derartige Listen!
33
Anwendungsbeispiele
Turm von Hanoi
In Kapitel 8 „Theoretische Informatik“ werden Rekursionen untersucht. Unter
anderem muss das „Turmspiel von Hanoi“ als Beispiel herhalten. Was das genau ist,
erfährt man auch hier:
http://www.matheprisma.uni-wuppertal.de/Module/Rekurs/index.htm (Turmspiel)
(Präsentation)
Es ist noch recht einfach, einen Turm aus drei Steinen nach den vorgegebenen
Regeln von links (1) nach rechts (3) über die Mitte (2) zu bringen.
Etwas schwerer wird es für vier Steine. Sollte man denken! Ist es aber nicht, denn
Sie haben es ja schon geschafft, einen Turm mit drei Steinen von (1) nach (3) zu
bringen:
Das ist hier bereits geschehen und nun ist es plötzlich ein Kinderspiel, den Rest zu
erledigen: Größte Scheibe auf (2), den Turm auf (3) wie eben, nur anders herum
zurück auf (1) …...
Das riecht ja förmlich nach Rekursion!
Bezeichnet man mit (a, b) den Zug von Stab a nach Stab b so sieht das rekursiv
konstruierte Programm so aus:
hanoi 0 s z h = []
hanoi n s z h = hanoi (n-1) s h z
++ [(s,z)]
++ hanoi (n-1) h z s
hanoi 6 1 3 2 heißt:
Ein sechs-Scheibenturm von Stab (1) nach Stab (3) mit Hilfe des Stabes (2).
s steht für Start-, z für Ziel- und h für Hilfsstab.
Und das Ergebnis:
Main> hanoi 6 1 3 2
34
[(1,2),(1,3),(2,3),(1,2),(3,1),(3,2),(1,2),(1,3),(2,3),(2,1),(3,1),(2,3),(1,2),(1,3),(2,3),(1,2),
(3,1),(3,2),(1,2),(3,1),(2,3),(2,1),(3,1),(3,2),(1,2),(1,3),(2,3),(1,2),(3,1),(3,2),(1,2),(1,3),(
2,3),(2,1),(3,1),(2,3),(1,2),(1,3),(2,3),(2,1),(3,1),(3,2),(1,2),(3,1),(2,3),(2,1),(3,1),(2,3),(
1,2),(1,3),(2,3),(1,2),(3,1),(3,2),(1,2),(1,3),(2,3),(2,1),(3,1),(2,3),(1,2),(1,3),(2,3)]
63 Züge! Versuchen Sie das mal von Hand!
(Man kann übrigens zeigen, dass die mit dem rekursiven Verfahren berechneten
Züge auch in ihrer Anzahl nicht unterschritten werden können!)
Damenproblem
Der Schachmeister Max Bezzel veröffentlichte 1848
in einer Schachzeitung folgende Frage:
Auf wieviele Arten kann man die maximal 8 Damen
auf einem Schachbrett verteilen, so dass sie sich
nicht gegenseitig bedrohen.
Zwei Jahre später nannte Franz Nauck in der
Leipziger Illustrierten die richtige Zahl.
Auch der Mathematiker Gauß interessierte sich für
das Problem, war aber nicht der „geistige Vater“ der
Aufgabe.
Später verallgemeinerte Nauck das Problem und
frage, wie viele Möglichkeiten es gibt, n Damen auf
einem nxn-Brett unterzubringen, ohne dass sich sich gegenseitig bedrohen.
(Bild : http://de.wikipedia.org/wiki/Damenproblem )
Die Anzahl der Lösungen für n = 12 wurde erste 1969 bewiesen!
Zwischen Oktober 2004 und Juni 2005 berechnete die Forschungsgruppe OASIS
das Problem für n = 25 und benötigte dazu 53 Jahr CPU-Zeit.
Der aktuelle Weltrekord für n wurde 2009 in Dresden aufgestellt: Für n = 26 gibt es
22.317.699.616.364.044 verschiedene Lösungen! Damit die Lösungszeit nicht
übermäßig lang würde, baute man einen für dieses Problem angepassten Rechner.
Hier können Sie versuchen, selbst einige Lösungen für ein 8x8-Brett zu finden:
http://www.hbmeyer.de/backtrack/achtdamen/autoacht.htm#up
Ziel ist es, eine Funktion in Haskell zu schreiben, die das n-Damen-Problem
(zumindest bis n =12) lösen kann.
Die Beschriftung der Felder wird wie im Bild links dargestellt
vorgenommen.
Man benötigt zunächst zwei Hilfsfunktionen:
Die Hilfsfunktion geht p1 p2 soll wahr zurückgeben, wenn sich
die Felder p1 und p2 nicht bedrohen und sicher [p1, p2,..] pn soll
wahr zurückgeben, wenn die Position pn den bisherigen
Positionen [p1, p2, …] hinzugefügt werden kann, ohne dass sich
die zugehörigen Damen gegenseitig bedrohen.
35
Aufgabe 17
Zeigen Sie, dass sich zwei Felder (a,b) und (c,d) genau dann bedrohen, wenn
a = c oder b=d oder a+b = c+d oder a-b = c-d gilt.
Wie könnte man mit dieser Erkenntnis eine Funktion geht p1 p2 erzeugen, die den
oben genannten Bedingungen genügt?
Hier ein Vorschlag für die beiden Hilfsfunktionen:
-- geht p1 p2 ist wahr, wenn sich p1 und p2 nicht bedrohen
geht :: Num a => (a,a) -> (a,a) -> Bool
geht (a,b) (c,d) = (a /= c) && (b /= d) && (a+b /= c+d) && (a-b /= c-d)
--sicher [p1,p2,..] pn ist wahr, wenn pn den bisherigen Positionen [p1,p2,..]
hinzugefügt werden kann
sicher :: Num a => [(a,a)] -> (a,a) -> Bool
sicher xs (n,m) = and [geht (i, j) (n, m) | (i, j) <- xs]
Wie Aufgabe 17 zeigt, kann man durch testen von vier Bedingungen die Bedrohung
eines Feldes p2 für p1 abklären.
Die Funktion sicher [p1, p2,..] pn greift auf die bekannte Funktion geht p1 p2
zurück: Es wird für alle p1, p2, … untersucht, ob sie mit pn verträglich sind.
Die Hauptfunktion wird, das dürfte niemanden überraschen, rekursiv definiert. Zur
einfacheren Handhabung benötigt man zu Beginn zwei Parameter:
damenRekursion k n - das sind die bis Spalte k verteilten Damen bei einem n x nFeld. Und wie bekommt man diese Damen-Verteilung?
Ganz einfach: Es sind damenRekursion (k-1) n, mit der Bedingung, dass die Dame
auf Position (k,m) sicher zu den vorhanden Damen steht. In Haskell-Code:
damenRekursion :: (Num a, Enum a) => a -> a -> [[(a,a)]]
damenRekursion 0 n = [ [ ] ]
damenRekursion k n = [xs ++ [(k,m)] | xs <- damenRekursion (k-1) n,
-- bisherige Lösung
m <- [1..n], -- suche über alle Zeilen
sicher xs (k,m) ]
-- nehme nur die erlaubten Felder
Damit man nicht unnötigerweise zweimal den gleichen Parameter eingeben muss,
wie etwa damenRekursion 8 8 definiert man die partielle Funktion damen n :
-- damen n damit n nicht zweimal eingegeben werden muss
damen n = damenRekursion n n
Wer nur die Anzahl der Lösungen wissen will, wird diese Funktion definieren:
-- Anzahl der Lösungen
wievieleLoesungen n = length (damen n)
Wieviele Lösungen gibt es für n = 12?
36
Grafik
Bisher macht Haskell nicht den Eindruck einer Sprache zu sein, mit der man auch
grafische Probleme lösen kann. Der Eindruck ist nicht gänzlich falsch, denn ein
Bildbearbeitungsprogramm wird man kaum in Haskell programmieren.
Sobald aber von grafischen Rekursionen die Rede ist, fällt die Wahl eben doch
wieder auf Haskell.
Man benötigt dazu bereits fertige Grafik-Module, die man durch Doppelklick auf
GraphicsUtils.hs (Tauschverzeichnis) aus dem Programmverzeichnis automatisiert
laden kann.
Lädt man jetzt noch durch :l Turtlegrafik , so stehen einem Befehle wie :
Gehe x und Drehe y zur Verfügung.
Ein Beispiel:
Voraussetzung: GraphicsUtils.hs ist durch Doppelklick geladen.
Nun schreibt man mit einem Editor die folgenden Zeilen und speichert den Code
unter gross_h.hs ab.
import Turtlegrafik
weg_h = [Gehe 140, Drehe 180, Gehe 65,
Drehe 90, Gehe 70, Drehe (-90),
Gehe 75, Drehe 180,
Gehe 140]
gross_h = kroete weg_h mitte
Jetzt mit „load moduls“ gross_h.hs laden.
Man kann nun testen, ob alles geklappt hat, indem man weg_h eingibt:
Main> weg_h
[Gehe 140.0,Drehe 180.0,Gehe 65.0,Drehe 90.0,Gehe 70.0,Drehe (-90.0),Gehe
75.0,Drehe 180.0,Gehe 140.0]
Eine Zeichnung darf man hier noch nicht erwarten, da weg_h ja nichts anderes ist,
als eine Liste von Befehlen.
Versuchen Sie herauszufinden, welcher Weg hier beschrieben wird, wenn man
davon ausgeht, dass man sich zu Beginn in der Mitte des Bereichs nach oben
orientiert aufhält und dass Linksdrehungen positive Winkel haben.
Durch Eingabe von gross_h wird mit Hilfe des kroete-Befehls der Weg gezeichnet:
37
Der Befehl mitte am Ende des
Programms bewirkt, dass die Position
wie zu Beginn eingenommen wird –
ebenfalls wieder mit Orientierung nach
oben.
Außerdem bewirkt der Befehl indirekt,
dass das Bild bis zu einem Tastendruck
stehenbleibt, da ja kein weiterer Weg
vorgegeben ist.
Aufgabe 17
Schreiben Sie ein Programm gross_a so
dass damit der Buchstabe A (siehe oben)
geschrieben wird. (Man muss ein wenig
mit den Winkeln und Längen
experimentieren.)
Kombinieren Sie danach die beiden
Programme, so dass AH geschrieben
wird. (Tipp: Zwei Listen kann man mit ++ zusammenfügen. )
Wer das geschafft hat, der bekommt auch „das Haus des
Nikolaus“ hin: Keine Linie zweimal gehen! (Bild links).
Natürlich wären diese banalen Zeichnungen mit
Haskell nicht weiter erwähnenswert, wenn man
nicht auch hier die Stärke der Programmiersprache
– die Rekursion – nutzen könnte.
Ein sehr einfaches Beispiel ist der „Stern“ rechts. Er
entsteht durch fortgesetztes Zeichnen von
Quadraten. Sternbild 83 ist im Bild links zu sehen.
Unten können Sie beobachten, wie die Rekursion
für Stern n funktioniert:
Es wird ein Quadrat wie in Stern 1 gezeichnet.
Dann dreht sich die Kroete um 360/n Grad.
Jetzt wird Stern (n – 1) gezeichnet. Na, -wenn das
keine Rekursion ist...
38
Sternbild 1
Sternbild 2
Sternbild 3
Sternbild 4
Der Quelltext ist sehr einfach. Versuchen Sie es selbst einmal! Eventuell benötigen
Sie eine Anregung in der nachfolgenden Lösung:
quadrat l = [Gehe l, Drehe 90, Gehe l, Drehe 90, Gehe l, Drehe 90, Gehe l, Drehe 90]
stern 0 = []
stern n = quadrat 80 ++ [Drehe (360/n)] ++ stern (n-1)
sternbild n = kroete (stern n) mitte
Es wäre hierbei nicht nötig gewesen, ein Quadrat mit beliebiger Seitenlänge zu
definieren. Allerdings kann man so auch ganz leicht Sterne von anderer Größe
zeichnen.
Es ist nicht schwer, sich nach diesem Muster eigene rekursive Bilder auszudenken.
Versuchen Sie es!
Besonders geeignet für die Demonstration rekursiv erzeugter Grafiken sind Fraktale,
wie z.B. die Kochkurve
(http://de.wikipedia.org/wiki/Koch-Kurve und http://de.wikipedia.org/wiki/Fraktal )
Fraktale sind selbstähnlich. Das heißt,
ein kleiner Ausschnitt der Bildes sieht
ähnlich aus wie das gesamte Bild.
Nebenstehend sehen Sie die
Kochkurven für n = 0, n = 1 und n = 2.
Da sie auch von der Länge len der
Kurve für n = 0 abhängt, braucht man
zwei Parameter. In Haskell sieht das
dann so aus: koch len n
Erzeut wird koch len n
umgangssprachlich so:
( n = Rekursionstiefe )
( len = Linienlänge )
koch len n
wenn n=0 dann Gehe len
sonst koch (len/3) (n-1)
39
Drehe (-60)
koch (len/3) (n-1)
Drehe 120
koch (len/3) (n-1)
Drehe (-60)
koch (len/3) (n-1)
Der Algorithmus muss nun nur noch in Haskell übertragen werden. Das geht fast eins
zu eins!
Aufgabe 18
Schreiben Sie den Haskell-Code für das Zeichnen der Kochkurve. Verzichten Sie
beim Zeichnen auf Werte für n, die größer als 6 sind. Zum Einen kann man die
Struktur bei der gegebenen Auflösung eh nicht mehr erkennen, zum Anderen wird
sich Ihr Rechner mit der Aufgabe wohl übernehmen!!
Im Wikipedia-Artikel zu Kochkurven wird auch die kochsche Schneeflocke erwähnt.
Diese bekommt man, wenn man drei Kochkurven mit je 120 0 gedreht zeichnet. Hier
die kochsche Schneeflocke 300 5
schneeflocke len n = koch len n
++ [Drehe 120]
++ koch len n
++ [Drehe 120]
++ koch len n
schneeflockezeichnen len n = kroete
(schneeflocke len n) mitte
Es gibt noch unzählige andere Fraktale,
die man mit Haskell erzeugen kann.
Nebenstehend ist der Pythagoras-Baum
dargestellt.
Erkennen Sie das Erzeugungsmuster?
Dann an die Arbeit! Realisieren Sie Ihre
Idee in Haskell.....
40
Binär-Verschlüsselung (Autor: Josef Eulitz)
Verschlüsselung auf Bitebene
In vorhergehenden Kapiteln haben wir unter anderem die Cäsar Verschlüsselung
behandelt.
Der entscheidende Nachteil dieser Verschlüsselung ist es, dass man damit lediglich
Texte mit Standard Satzzeichen verschlüsseln kann, deshalb soll in diesem Teil die
sogenannte xor-Verschlüsselung erklärt werden. Sie kann auf jede Art von Daten,
in binärer Darstellung angewandt werden kann.
Bei der xor Verschlüsselung wird auf den zu verschlüsselnden Datensatz über einen
festgelegten Schlüssel auf jedes Bit eine xor-Operation aus geführt, somit ändert
sich der Inhalt des Datensatzes komplett.
Das hat gegenüber anderen Verschlüsselungsmechanismen einen entscheidenden
Vorteil: Da xor wie eine schriftliche Addition ohne Übertragungsbit funktioniert,lässt
sich der Datensatz durch wiederholtes ausführen der xor-Operation mit dem gleichen
Schlüssel wieder herstellen.
Der Schlüssel
Der Schlüssel bei einem solchen Verfahren sollte möglichst lang sein, da ein weniger
deterministischer Schlüssels die Chancen einer Entschlüsselung stark reduziert.
Ideal wäre es natürlich, ein Onetimepad zu erstellen. Allerdings wäre dies relativ
aufwendig und unhandlich. Deshalb hat man sich überlegt einen kürzeren Schlüssel
zu verwenden, der mit einem Schlüsselgenerator auf die erforderliche Länge
gebracht wird. In meinem Fall habe ich den Schlüssel aus einem Teil der FibonacciFolge entnommen.
Nun in Haskell
Im folgenden Beispiel wird gezeigt, wie man, mit Hilfe des oben beschriebenen
Verfahrens einen String in Haskell verschlüsselt. Grundsätzlich wäre das auch mit
jedem anderen Datentyp möglich, aber man muss diesen erst in eine geeignete
Binärdarstellung bringen. Ich habe Dazu den Datentyp Boolean gewählt, da er mit
den Werten wahr oder falsch genau 1 Bit darstellt.
Programm-Entwurf
Weil man in Haskell rekursiv arbeitet, ist es wahrscheinlich am einfachsten, das
Design von unten nach oben zu erstellen:
1. xor Funktion
2. xor auf [Bool] map Funktion
3. Funktion 2. auf [[Bool]] und Key Mapfunktion (Vorsicht, das erste Bit muss
unverändert bleiben, da es sich hier nicht um eine festgesetzte Parzellenlänge
handelt.)
41
4. Keygenerator
1. Zahlengenerator, bei dem die Zahlen relativ gleichmäßig verteilt sind
2. Binärkonverter um den Schlüssel Integer in Binär umzuwandeln
3. Funktion, die den Schlüssel auf die passende Länge bringt
5. Funktion die den Schlüsselgenerator mit Funktion 4 verknüpft
6. Funktion die den zu verschlüsselnden Datentyp in [[Bool]] Schreibweise
überführt
1. Integer zu Binär Funktion (kann auch die vom Schlüsselgenerator
wieder verwendet werden)
2. Mapfunktion 1. auf ein Integer Array ( weil Chars recht leicht in Integer
werte gewandelt werden können)
3. wandle Char-Werte in einem String in Integer-Werte und verknüpfe sie
zu [Integer]
7. Eine Reversefunktion für die Formatierung, um einen gleich formatierten
Datensatz ausgeben zu können, wie man eingegeben hat.
8. alles umschließende Toplevel Funktion, die alle Parts zusammen nimmt, um
eine größere Modularität zu erreichen und spätere Ergänzungen einfacher
hinzu fügen zu können.
1. die Xor-Funktion
da wir mit Boolean-Werten rechnen, lässt sich diese Funktion recht einfach
realisieren, und zwar
xor::Bool->Bool->Bool
xor a b = (a /= b)
Die Funktion gibt True zurück, wenn die 2 Booleans auf die xor angewendet wurden
ungleich sind.
2.Map auf einzelne Parzellen
xorcryptphaseI::[Bool]->[Bool]->[Bool]
xorcryptphaseI [] [] = [] --abbruchbedingung
xorcryptphaseI [] (k:ey) = [] --abbruchbedingung
xorcryptphaseI (ar:ay) [] = ar:ay --abbruchbedingung
xorcryptphaseI (ar:ay) (k:ey) = xor ar k : (xorcryptphaseI ay ey)
Der map-Befehl wäre hier nicht möglich, da über 2 Listen iteriert werden muss.
42
3.Map mit Ausnahme
xorcrypt::[[Bool]]->[Bool]->[[Bool]]
xorcrypt [] [] = [] --abbruchbedingung
xorcrypt [[]] [] = [] --abbruchbedingung
xorcrypt (b:ba) key = xorcryptphaseI b (((head b)==False):key) : xorcrypt ba
(drop (length(b)-1) key)
Hier wird Funktion 2 auf jede Parzelle mit dem zugehörigen Teil des Schlüssels angewendet, nur das Kopfbit jeder Parzelle muss gleich bleiben, weil sich sonst die
Länge des Datensatzes eventuell ändert.
4.Der Schlüsselgenerator
Wie der Schlüsselgenerator konzipiert wird, ist jedem selber überlassen, er muss nur
für einen Verschlüsselungsstandart der gleiche sein. In dem Fall generiere ich einen
Schlüssel aus der Binärdarstellung einer zahl in der Fibonaccifolge.
Für die Berechnung von Fibonacci stellen habe ich mir einen Algorithmus aus dem
Internet besorgt, mit dem die Berechnung größerer zahlen mit gleicher Stackgröße
möglich wird.
4.1. Der Zahlengenerator
Der Algorithmus gibt (fibo n,fibo n-1) zurück.
fib::Int -> (Integer,Integer)
fib 0 = (0, 0)
fib 1 = (1, 0)
fib n | even n = ( (k1+k3)*k2, k1^2+k2^2 )
| odd n = ( k3^2+k2^2, (k1+k3)*k2 )
where (k2, k1) = fib$div n 2
k3 = k2 + k1
43
4.2. die Anweisung zur Binärumwandlung des erhaltenen Wertes
fibobool::Int->[Bool]
fibobool 0 = [False]
fibobool n = (integerbin.fst.fib) n
wobei Integerbin durch folgende Funktion beschrieben wird:
invbin:: Integer -> [Bool]
invbin 1 = [True]
invbin f = (mod f 2 /= 0) : invbin(div f 2)
integerbin:: Integer ->[Bool]
integerbin f = reverse(invbin(f))
Dieser Funktion liegt ein Algorithmus zugrunde, den man sich merken sollte, falls
man irgendwann einmal vor hat, Informatik zu studieren, da in der
Einführungsvorlesung mindestens 2 Übungsblätter zum Umrechnen von
Zahlensystemen aufgegeben werden, und zwar so, dass die Zahl im anderen
Zahlensystem, sich von rechts nach links folgendermaßen aufbaut:
Man muss sie durch die Zahl, die den Namen des Zahlensystems gibt teilen. In
diesem Fall also durch 2 (Binär). Dann den Rest von rechts nach links aufschreiben.
Dies lässt sich am besten mit modulo realisieren. Nun teilt man die erhaltene Zahl
wieder durch 2 usw. das würde z.b. Bei 10 folgendermaßen aussehen:
(ich verwende für modulo '%' und für div (ganzzahlige Division) '/', wie es in den
meisten Programmiersprachen üblich ist.)
10%2 = 0 –10/2→ 5%2 =1 – 5/2 → 2%2 = 0 – 2/2 → 1%2 =1.
Wenn man nun das Ganze von rechts nach links aufschreibt also
1010
hat man 10 in Binärdarstellung.
Probe:
1010 → 10
8421 → Stellen
8+2 = 10
44
4.3.Der Schlüssel muss noch auf die richtige Länge gebracht werden.
Es wird festgelegt, dass, falls der Schlüssel nicht lang genug ist, er wiederholt wird.
keygen::Int->Int->[Bool]
keygen 0 0 = [] --Abbruchbedingung
keygen 0 n = take n (cycle[False]) --Abbruchbedingung
keygen n 0 = [] --Abbruchbedingung
keygen nu anz = take anz (cycle(list))
where list = (fibobool(nu))
Mit dieser Funktion habe ich durch where list = fibobool(nu) den gesamten
Keygenerator in einer Funktion verknüpft. Man kann diesen Wert aber auch als
Parameter übergeben, um mehrere Zahlengeneratoren zu verwenden.
Alternativ könnte man zum Beispiel einen Pseudozufallsgenerator machen, dessen
Startwert (Seedvalue) dann beim en- und decodieren gleich sein muss und der keine
äußeren Faktoren mit einbezieht.
5. Verknüpfung des Schlüsselgenerators mit der Verschlüsselungslogik
cryptJBoolArray::[[Bool]]->Int->[[Bool]]
cryptJBoolArray bo ky = xorcrypt bo (keygen ky ((gesamtlength bo)-(length
bo)) )
Da die Headelemente der einzelnen Parzellen gleich bleiben, benötigt man dazu
keinen Key über die volle Länge des Datensatzes, sondern nur einen mit der Länge
n bei dem n = die Länge des Datensatzes minus die Anzahl der Parzellen ist.
Außerdem benötigt man noch eine Funktion die die Länge von verschachtelten
Arrays errechnet .
gesamtlength::[[a]]->Int
gesamtlength [] = 0
gesamtlength (x:xs) = length(x)+gesamtlength(xs)
45
6. die Überführung von String in [[Bool]]
6.1 Integer zu Binär-Funkion
Um Integer in Boolean um zu wandeln, kann man wieder die Umwandlungsfunktion
vom Keygenerator verwenden, allerdings muss hier noch eine neue Reverse Funktion geschrieben werden, da die Funktion fromenum, die Char in Int um wandelt,
Chars eben nur in Int umwandelt und nicht in Integer. Also:
ibin:: Int ->[Bool]
ibin f = reverse(invbin(toInteger(f)))
6.2 Map
strbin:: String->[[Bool]]
strbin (xs) = map ibin (strIntegerar xs)
6.3 Char zu Integer
Wie man es schon von der Cäsar -Verschlüsselung kennt: Man nehme die integerWerte der einzelnen Chars im String
strIntegerar:: String->[Int]
strIntegerar [] = []
strIntegerar (x:xs) = fromEnum(x):strIntegerar(xs)
Hierbei spielt es keine Rolle, um welche Zeichen es sich handelt, da wir später
variabel große Felder in unserem verschlüsselten Array haben werden, bzw. es
wieder in einen String umwandeln werden.
7. die Reverse -funktion
Dieser Teil bleibt weitestgehend unkommentiert, da es sich um die Umkehrung des
Vorherige Parts handelt.
revbin:: [Bool] -> Int --binary wieder in integer
revbin [] = 0;
revbin (x:xs)
|x = 2^(length(xs))+revbin(xs)
|otherwise = revbin(xs)
46
Wenn True dann wird die zugehörige Stelle hinzu addiert.
intarstring::[Int]->String
intarstring [] = []
intarstring (i:nt) = toEnum(i):intarstring(nt)
Int-> Char auf Array
boolstring::[[Bool]]->String
boolstring [] = []
boolstring (x:xs) = intarstring((map revbin (x:xs)))
Toplevel Funktion drauf und fertig.
8. Toplevel Funktion
Da ich im vorherigen Bereich schon relativ viel verknüpft habe, fällt diese Funktion
relativ simple aus
cryptString::String->Int->String
cryptString xs key = (boolstring.cryptJBoolArray (strbin xs)) key
Bemerkung:
Ich habe auch ein Haskell-Skript Binärverschlüsselung.hs erstellt, in dem der
komplette Code und einiges mehr enthalten ist.
Um einige Funktionen aus zu probieren braucht man es nur in den Haskell
Interpreter/Compiler zu importieren, und die gewünschte Funktion auf zu rufen, das
zugehörige Script ist weitestgehend dokumentiert.
Man kann auch eigene Funktionen einfügen, um die Funktionalität zu erweitern,
allerdings stimmen dann die immer wieder rein kommentierten Zeilenzahlen nicht
mehr.
47
Microsofts F #
Wine neue funktionale Programmiersprache
(Autor: Tristan Kreuziger)
1. Geschichte und Fakten
F # ist eine funktionale Programmiersprache, die von Microsoft entwickelt wurde,
genauer gesagt von Microsoft Research als Forschungsprojekt. Sie ist funktional, hat
aber gleichzeitig imperative und Objekt orientierte Elemente.
Der erste Release war im Jahre 2002. Inzwischen ist F # aber dem
Forschungsstadium entwachsen und wird von Microsofts Entwicklungsabteilung
fortgeführt.
Seit November 2010 steht F #, der zugehörige Compiler und die Bibliotheken der
Welt nun unter der Apache-2.0-Lizenz zur Verfügung.
(http://de.wikipedia.org/wiki/Apache-Lizenz_2.0)
F # kann sowohl in einer Konsole ausgeführt werden als auch in Microsofts Visual
Studio integriert werden. Der Vorteil davon liegt auf der Hand: Die volle
Unterstützung aller Features in Visual Studio, wie z.B. Intellisense oder Debugging.
Erstmals in Visual Studio 2008 konnte F # nachträglich installiert werden, in der
aktuellen Version 2010 ist es automatisch enthalten, nur die Express-Versionen
müssen nachrüsten.
2. Download und Installation von F #
Das reine F # bietet nur eine schlichte Konsole, die sich von Windows' cmd.exe nicht
sonderlich unterscheidet.
Microsoft bietet aber ein F # Add-In für alle Nutzer von Visual Studio an, auch
diejenigen, die "nur" eine Express-Version verwenden. Für Nutzer von Visual Studio
2010 (Professional & Ultimate) ist der Aufwand am geringsten, nämlich gleich null. F
# kommt bereits voll integriert bei ihrer Installation mit.
Bei den Express-Versionen muss man zuerst die "Visual Studi 20XX Shell"
installieren passend zur eigenen Version. Danach kann man F # 2.0 herunterladen.
Man findet einen neuen Eintrag im Startmenü, der "Visual Studio 20XX" heißt. Wenn
man dieses startet, kann man ein neues Projekt von VisualF# erstellen. Alles weitere
funktioniert wie bekannt aus Visual Studio.
Alle aktuellen Downloads finden sich hier: http://research.microsoft.com/enus/um/cambridge/projects/fsharp/release.aspx
3. Typische Sprachmerkmale
Im Folgenden möchte ich die wichtigsten Sprachmerkmale von F # darstellen. Oft
gibt es verschiedene syntaktische Möglichkeiten, um die selbe Sache zu schreiben.
48
Das hängt damit zusammen, dass in F # die Präprozessordirektive #light sehr
populär ist. Inzwischen ist sie auch standardmäßig aktiviert und wenn man den
ausführlichen Syntax verwenden will, dann muss man das explizit kennzeichnen:
Als Nachschlagewerk für den gesamten Syntax von F # empfehle ich wärmstens die
Seiten auf msdn: http://msdn.microsoft.com/de-de/library/dd233154.aspx
3.1 Datentypen in F #
In F # stehen dem Programmierer alle Datentypen aus den .NET Bibliotheken zur
Verfügung, nicht nur die einfachen wie Integer, Float, Double und String, sondern
auch komplexe wie z.B. ein FileStream:
Datentypen in F # können bei der Deklaration von Variablen und Funktionen explizit
angegeben werden oder man kann es dem Compiler überlassen, die
entsprechenden Datentypen herauszufinden. F # macht den Datentyp dann implizit
an der Art der Daten fest, die zurückgegeben werden und anhand der Operatoren,
die auf die Variablen verwendet werden. Das kann zu Mehrdeutigkeiten führen: Der
Operator + kann auf alle Arten von Zahlen angewendet werden, aber auch auf
Strings. Der Operator – hingegen nur auf Zahlen. Der Compiler ergänzt in so einem
Fall den Typ Integer, also erfolgt ein Fehler, falls man versucht dieselbe Funktion auf
Strings anzuwenden. Wie auch bei Haskell ist man auf der sicheren Seite, wenn man
immer die Datentypen angibt.
Unabhängig davon, ob man ihn angegeben hat oder nicht, wird der Datentyp jedes
Parameters und jeder Funktion angezeigt, wenn man in VisualStudio mit der Maus
über ihn fährt. Außerdem wird der absolute Namespace angegeben und die Module,
in denen die Funktion steht.
49
3.2 Deklaration von Funktionen
Alle Deklarationen (mit Ausnahme von Klassen, Typen...) werden mit dem
Schlüsselwort let deklariert. Weil F # eine funktionale Programmiersprache ist, muss
sich der imperative Programmier von dem Gedanken der Variable verabschieden. In
F # gibt es ausschließlich Funktionen.
Eine Funktion erhält einen Namen und nimmt verschiedene Parameter entgegen.
Diese verwertet sie zu einem Ergebnis, das sie zurückgibt.:
Den Datentyp von Parametern lässt sich mit einem Doppelpunkt und angefügtem
Datentyp festlegen. Bei einem Parameter kann man auf Klammern verzichten, aber
sobald es mehrere sind, sind Klammern syntaktisch zwingend erforderlich.
Außerdem verbessern sie die Übersicht erheblich:
Als Parameter können nicht nur Zahlen oder Zeichen übergeben werden, sondern
auch Funktionen. Diese können dann intern wieder angewendet werden, wie auch
schon aus Haskell bekannt. Dieses Phänomen, dass Funktionen als Parameter
auftreten können, bezeichnet man in der Informatik als Funktion höherer Ordnung
und findet sich typischerweise in den funktionalen Programmiersprachen.
Die starken imperativen Einflüsse in F # bemerkt man an den Möglichkeiten der
Verwendung von Variablen. Diese existieren in funktionalen Programmiersprachen
eigentlich nicht.
Eine Möglichkeit, die funktional ist und lediglich einen Kompromiss für imperative
Programmierer darstellt, ist eine Funktion mit festem Rückgabewert:
Im Unterschied zu einer klassischen Variablen, wie man sie aus der imperativen
Programmierung kennt, ist dieser Ausdruck eine Deklaration. Man kann einen
Bezeichner nur einmal deklarieren, eine erneute Zuweisung von pi würde einen
Fehler produzieren. Um dies zu umgehen, kann man bei der Deklaration einer
Funktion das Schlüsselwort mutable verwenden, um im Nachhinein den Wert
verändern zu können. Dies ist mit dem Operator <- möglich.
50
Nach der Ausführung dieses Beispiels gibt die Variable x den Wert 200 zurück.
Eine zweite Möglichkeit sind Referenzzellen. Diese stellen Speicherorte dar, die
veränderbare Werte enthalten. Die Deklaration erfolgt mit dem Schlüsselwort ref.
Um den Wert einer solchen Zelle zu erfahren, wendet man den ! Operator an. Um
den Wert der Zelle zu ändern, gibt es den := Operator:
Die Ausgabe des Programms nach dem Durchlauf lautet "5" und "10".
3.3 Lambda-Ausdrücke
Als Lambda-Ausdruck bezeichnet man in F # eine anonyme Funktion. Sie wird mit
dem Schlüsselwort fun deklariert. Wie ihr Name schon sagt, hat sie keinen Namen
und wird lokal definiert. Deshalb kann sie auch nur einmal aufgerufen werden.
Ein einfaches Beispiel dafür:
Auch bei Lambda-Ausdrücken kann man den Datentyp der Parameter definieren, da
sie aber meist nur für einen ganz speziellen Fall deklariert werden, ist das meistens
nicht notwendig.
Diese anonymen Funktionen lassen sich in normale Funktionen integrieren, wofür
man ansonsten noch eine zusätzliche Funktion deklarieren müsste. Wie in Haskell
kommt das in F # besonders häufig bei Listenoperationen zum Einsatz:
51
Mehr zu Listen und Listenoperationen gibt es im nächsten Abschnitt.
3.4 Listen, Tupel und Arrays
In F # werden wie auch in Haskell Listen und Tupel unterstützt und mit zahlreichen
Funktionen von Haus aus ausgestattet. Ein weitere Möglichkeit sind die Arrays.
3.4.1 Listen
Eine Liste zu deklarieren, funktioniert ähnlich wie eine Variable oder Funktion:
Alle Elemente in einer Liste müssen vom selben Typ sein. Listen können alle
Datentypen aufnehmen, die wir in .NET kennen. Dabei ist zu beachten, dass die
Objektorientierung uns hier weitere Möglichkeiten an die Hand gibt: Instanzen
abgeleiteter Klassen können in einer Liste ebenso auftauchen wie Instanzen der
ursprünglichen Klasse selbst.
Beispiel:
Soll bei der Deklaration der Datentyp mit angegeben werden, muss das Wörtchen
List angefügt werden:
52
Listen können auch mit einem bestimmten Bereich von Zahlen belegt werden. Die
Liste enthält dann alle Werte vom angegebenen Startwert bis zum Schlusswert. Im
folgenden Beispiel enthält die Liste am Ende die Werte von 1 bis 10:
Listen können auch durch sogenannte Sequenzausdrücke erstellt werden. Diese
ähneln ein bisschen den anonymen Funktionen. So lässt sich eine Liste bequem mit
unregelmäßigen Werten füllen. Man könnte auf diese Weise auch eine Funktion zur
Generierung der Werte heranziehen.
Zur Be- und Verarbeitung von Listen stellt F # dem Benutzer zwei Operatoren zur
Verfügung: :: und @
Der ::-Operator fügt einer Liste ein Element hinzu, allerdings immer nur am Kopf.
Man gibt ein Frontelement an und eine beliebige Anzahl an anderen Elementen
alleine oder in Form einer Liste.
Der @-Operator verknüpft zwei Listen. Selbstverständlich müssen bei beiden
Operatoren die Typen der Elemente und Listen übereinstimmen.
Beispiel:
53
Die erste Liste enthält die Zahlen 100 bis 110. Die zweite Liste enthält alle Elemente
der ersten Liste plus zusätzlich die Zahlen von 111 bis 120.
Zusätzliche Funktionen für Listen enthält das Modul List. Dort findet man auch viele
Funktionen, die wir schon aus Haskell kennen. Die wichtigsten Funktionen sind die
folgenden:
Die Bedeutungen sollten aus Haskell größtenteils bekannt sein, wenn nicht hilft
Intellisense gerne weiter.
Einige Funktionen sind statisch, d.h. sie sind nur im Modul List verfügbar, andere
sind nur als Funktionen einer Instanz von List verfügbar.
3.4.2 Tupel
Die zweite Struktur um mehrere Werte zu halten, kennen wir auch bereits aus
Haskell: die Tupel. Sie können beliebig viele Instanzen beliebiger Datentypen
enthalten. Sie werden genauso vereinbart wie auch in Haskell:
Um mit Tupeln zu arbeiten, weist man ihren Elementen Namen zu. Mit deren Werten
kann man dann arbeiten. Besonders effektiv sind Tupel, wenn sie mittels Pattern
Matching analysiert werden. Das kommt zwar erst im übernächsten Abschnitt, aber
hier schon einmal ein kleines Beispiel:
54
Die einfachsten Funktionen, die von F # mitgeliefert werden sind: fst und snd. Sie
liefern jeweils das erste und das zweite Objekt des Tupels.
Für jedes weitere Element muss man sich selbst eine Funktion definieren nach
folgendem Muster (ein Unterstrich steht für ein beliebiges Element, das nicht
überprüft wird):
3.4.3 Arrays
Ein Array in F # ist das gleiche wie in allen anderen .NET Sprachen auch: eine
nullbasierte änderbare Sequenz fester Größe von aufeinander folgenden Objekten,
die alle denselben Typ haben müssen.
Sie werden sehr ähnlich wie Listen gehandhabt und unterscheiden sich auch im
Syntax nicht all zu sehr. Die Deklaration ist fast gleich:
Genau wie Listen können bei Arrays auch Zahlenräumen, z.B. 1 .. 10, angegeben
werden. Auch die Sequenzausdrücke stehen dem Benutzer hier wieder zur
Verfügung. Desweiteren stellen die F #-Bibliotheken viele Funktionen zum
Initialisieren von Arrays mit vorgegebenen Werten bereit, auf die ich nicht weiter
eingehen werde. Zwei davon sind Array.create und Array.init.
Auf Arrays kann man wie gewohnt aus anderen Programmiersprachen über einen
Index zugreifen (Achtung, auf den Punkt vor den Klammern achten!):
55
Jeder Array ist ein Element aus dem .NET-Framework und verfügt daher über die
alle Funktionen, die wir von dort bereits kennen. Das Modul Array ist auf
eindimensionale Arrays ausgelegt, für mehr Dimensionen stehen noch die Module
Array2D, Array3D und Array4D zur Verfügung.
3.5 Pattern Matching
Das Pattern Matching ist ein Vorgang bei dem überprüft wird, was für eine Art von
Daten eingegeben wird. In F # wird es verwendet, um Daten mit logischen Strukturen
zu vergleichen.
Eine Pattern Matching Struktur wird durch die Worte match expression with
eingeleitet. Dann folgen mehrere Bedingungen und unterschiedliche Ergebnisse:
In dem Beispiel wird eine Zahl x überprüft und je nach Ergebnis wird etwas anderes
danach getan. Man kann das auch in etwas komplexeren Zusammenhängen
verwenden, z.B. wenn man Enumerationstypen verwendet:
56
Hier wird die Farbe in Worten zurückgegeben, die vorher als Aufzählung definiert
wurde. Der Unterstrich steht für ein beliebige Element, das nicht weiter benannt wird,
genauso wie bei Tupeln.
Man kann Pattern Matching auch sehr effektiv auf Listen und Tupel anwenden, die
dann einzeln in ihren Elementen verglichen werden können:
Es gibt viele Muster, um verschiedene Daten vergleichen zu können dabei sind die
klassischen AND (&), OR (|) und noch viele andere enthalten. Eine komplette Liste
davon findet sich hier: http://msdn.microsoft.com/de-de/library/dd547125.aspx
3.6 Objektorientierung
F # kann komplett Objekt orientiert programmiert werden, das ist besonders dann
interessant, wenn F # Module in anderen .NET Sprachen weiter verwendet werden
sollen.
3.6.1 Namespaces
Alle Module, Klassen und Typen in F # können in Namespaces gekapselt werden.
Ein Namespace wird so angelegt:
Die Deklaration muss in der ersten (geschriebenen) Zeile einer Quelldatei stehen und
alles nachfolgende wird in diesem Namespace zusammengefast. Es können auch
mehrere Namespaces in einer Datei vereinbart werden. Sollen diese ineinander
geschachtelt sein, muss der volle Name angegeben werden.
Beispiel:
57
Hiermit wurde der Namespace Outer deklariert, der einen inneren Namespace Inner
besitzt. Zusätzlich wurde der unabhängige Namespace Elsewhere deklariert.
Die Deklaration kann aus Bequemlichkeitsgründen auch weggelassen werden, wenn
z.B. nur ein einziges Modul in der Datei vorhanden ist. Der Name des Namespaces
wird dann implizit bei der Deklaration des Moduls oder Typs mit angegeben:
(mehr Informationen zu Modulen im nächsten Abschnitt)
Der oberste Namespace heißt global und ist vordefiniert. Man kann ihn verwenden,
um eigenen Code der obersten Namespace-Ebene zuzuordnen oder um
Namenskonflikte zu vermeiden:
Die ganze Geschichte mit Namespaces wäre sehr umständlich, wenn es nicht auch
eine einfache Möglichkeit gäbe, sie zu verkürzen. Dafür gibt es den Befehl open,
dahinter hängt man den Namen des Namespaces (oder Moduls), der eingebunden
werden soll:
Das ist vergleichbar mit dem using in C++/C# oder Import in VB.NET.
Um eine Basis-Funktionalität zur Verfügung zu stellen, sind einige Namespaces
standardmäßig eingebunden: Microsoft.FSharp.Core,
Microsoft.FSharp.Core.Operators, Microsoft.FSharp.Collections,
Microsoft.FSharp.Control und Microsoft.FSharp.Text.
58
3.6.2 Module
Ein Modul fasst eine Gruppe von F#-Codeteilen zusammen. Beim Import in .NET
wird daraus eine Klasse mit ausschließlich statischen Membern erstellt.
Es gibt zwei leicht unterschiedliche Arten von Modulen. Das erste ist eine
Moduldeklaration der obersten Ebene. Diese Deklaration taucht ganz am Anfang der
Datei auf und enthält alles, was danach deklariert wird. Es ist nicht nötig einen
Zeileneinzug bei ihren Membern vorzunehmen.
Anders verhält es sich mit einer " lokalen Moduldeklaration"1. Diese kann innerhalb
eines Namespaces oder eines anderen Moduls auftreten. Alles, was in ihm deklariert
sein soll, muss einen Zeileneinzug relativ zu ihm erhalten:
Das Modul Outer ist ein Modul oberster Ebene, darin finden sich zwei Funktionen
definiert und ein inneres Modul.
Wenn man eine ganz kurze Quelltextdatei schreibt und auf Namespace und Module
verzichtet, werden sie implizit trotzdem eingesetzt:
Um auf Code in einem Modul zuzugreifen, muss man genauso verfahren, wie bei
jeder anderen Objekt orientierten Programmiersprache:
Namespace1.Namespace2.ModuleName.Identifier
3.6.3 Typen & Typerweiterungen
In F # ist es möglich Typen zu deklarieren, die intern noch einmal große
Unterschiede haben können. Es gibt zahlreiche verschiedene Arten: Klassen,
Unterscheidungs-Unionen, Datensätze und Strukturen. Desweiteren haben wir in
einem Beispiel auch schon Aufzählungen kennen gelernt.
59
Ich werde all diese verschiedenen Strukturen nicht weiter darlegen, weil sie sich
häufig syntaktisch nur sehr leicht unterscheiden und für sehr spezielle Aufgaben
ideal geeignet sind. Außerdem glaube ich, dass Module allein ausreichen, um F # in
anderen .NET Anwendungen zu nutzen.
3.7 Kommentare
Ein kurzes Wort zu Kommentaren in F # möchte in an dieser Stelle noch anbringen.
Gewöhnliche Quelltext-Kommentare können mit // einzeilig geschrieben werden oder
mit (* bis *) mehrzeilig geschrieben werden.
Desweiteren kann man XML-Kommentare schreiben, die auch für die Dokumentation
des Quelltexts nützlich sind. Sie beginnen mit /// vor einer Deklaration.
4. Vergleich zu Haskell
4.1 Gemeinsamkeiten
Wer sich mit funktionaler Programmierung auseinander gesetzt hat, wird keine
Schwierigkeiten mit dem Erlernen von F # haben. Das Meiste ist wohl ohne Erklärung
zu verstehen, das einzige, was man wirklich lernen muss, ist der neue Syntax und
selbst der weist viele Parallelen auf. Diese Ähnlichkeiten können wir uns an ein paar
Beispielen ansehen.
Beispiel 1 Funktionen:
Haskell:
f x = x * x
F Sharp:
let f x = x * x
Beispiel 2 Anonyme Funktionen:
Haskell:
(\x -> x * x)
F #:
fun x -> x * x
Beispiel 3 Listen:
Haskell:
-- leere Liste
60
[]
-- beliebige Liste
[1 .. 10]
-- Listen-Operatoren
list = [1,2,3]
head list
tail list
length list
map list
F #:
-- leere Liste
[]
-- beliebige Liste
[1 .. 10]
-- Listen-Operatoren
list = [1; 2; 3]
List.head list
List.tail list
List.length list
List.map list
Ebenso wie Haskell ermöglicht F # kleine Bequemlichkeiten wie Currying und
partielle Definition von Funktionen.
Wir können also sehen, dass sie Unterschiede zwischen F# und Haskell geringer
Natur sind und das meiste sowieso nur syntaktisch. Logisch funktionieren beide
Sprachen sehr ähnlich.
4.2 Unterschiede
Unterschiede finden wir am meisten im Bereich der Objektorientierung, die in Haskell
nicht ohne weiteres möglich ist. In der Tat gab es jedoch ein Projekt OHaskell,
dessen Ziel es war Objektorientierung in Haskell einzubringen. Aus dem Projekt
entwickelte sich jedoch etwas anderes: Timber (http://timber-lang.org/index.html).
Wenn man sich diese Sprache anschaut, wird man sehr stark sowohl an Haskell als
auch an F # erinnert.
Als ein weiterer oft genannter Unterschied hört man oft, dass Haskell „lazy“ ist im
Gegensatz zu F #. Das bedeutet, dass manche Ausdrücke nicht zwingend bis zum
Ende ausgewertet werden, wenn ihr Ergebnis schon feststeht. Dieses Verhalten lässt
sich allerdings bei beiden Programmiersprachen explizit steuern, also zählt es nicht
mehr als wirklicher Unterschied.
4.3 Caesar-Verschlüsselung in F #
Die bereits aus dem Unterricht bekannte Caesar-Verschlüsselung haben wir bereits
in Delphi und Haskell umgesetzt. Im Folgenden zeige ich, wie man diese
Verschlüsselung in F # umsetzen kann. Beim Vergleich mit dem Haskell-Quelltext
fällt auf, wo die Unterschiede liegen. Das meiste bleibe genau gleich.
61
Ein paar Hinweise sind noch notwendig: In Haskell brauchen wir die char.hs
Erweiterung, in F # können wir die implizite Konvertierung von den .NET Datentypen
nutzen. Das funktioniert einfach mit in Klammern vorgestelltem Datentyp. So können
wir einfach char in int umwandeln und erhalten den ASCII-Code.
Eine weitere Schwierigkeit taucht auf, wenn wir unsere Verschlüsselung auf einen
String anwenden wollen. Wir verwenden Funktionen, die auf Listen angewendet
werden müssen, aber in F # ist ein String ein anderer Typ als eine Liste von
Charactern. Bei Haskell ist das derselbe Typ, aber wir müssen uns erst eine Funktion
schreiben, die einen String in eine char List verwandelt.
Das Ergebnis sieht dann so aus:
So wird die gesamte Funktionalität in einem Modul gebündelt und kann auch von
anderen .NET Programmen verwendet werden. Ein Anwendungsbeispiel könnte so
aussehen:
62
5. In der Praxis - Haskell oder F #
Die spannende Frage zum Schluss ist natürlich, ob das neue F # gegenüber dem
traditionellen Haskell eine Chance auf dem Markt hat. Letztendlich entscheidet
darüber der Benutzer.
Dank der absoluten Einbindung von F # in .NET ist es möglich kleine Programmteile
funktional zu schreiben, den Kern der Anwendung in C # zu verwirklichen und die
grafische Oberfläche mit VisualBasic zu entwerfen.
Chris Smith von Microsoft hat dazu Folgendes gesagt:
"It is important to note that F# supports nearly every feature that C# does. So when
you use F# it isn’t an all-or-nothing type of proposition. You don’t have to throw away
your existing code and move everything over to F#. In fact, we expect F# code to
primarily be used as a class library integrated into a larger software package.
Perhaps some backend server component is written in F# while the rest of your code
remains in C#/PHP/Smoke Signals/whatever."
Wenn man nun rückblickend betrachtet, dass Haskell und F # zwar ein paar
Unterschiede aufweisen, aber alles in allem auch sehr viel gemeinsam haben, dann
stellt sich die Frage, was beliebter ist und warum.
5.1 An der Universität
Die Universitäten halten am streng funktionalen und traditionellen Haskell fest. F #
erscheint, wenn man nur über den funktionalen Teil spricht, ein bisschen weniger
ausgeprägt als Haskell. Zu viel Objektorientierung und imperative Struktur ist in F #
enthalten, das Funktionale ist nicht unbedingt zwingend wie bei Haskell.
Die Möglichkeiten der Integration in andere Programme, die bei F # gegeben sind
dank des .NET Frameworks, sind für Pädagogen eher zweitrangig. Es geht darum
den mathematischen Aspekt einer funktionalen Programmiersprache zu
verdeutlichen. Alles "andere" ist dabei überflüssig. Man will keine großen Programme
verwirklichen, sondern kleine mathematische Probleme mit wenig Zeilen Quellcode
lösen und sich das Ergebnis direkt in Haskells Editor ansehen. Dafür mag Haskell
sogar eher geeignet sein als F #, in eine große komplexe Anwendung integriert man
seinen F # Quelltext dafür wesentlich schneller.
63
5.2 In der Wirtschaft
Gerade die Wirtschaft sieht natürlich ihre Vorteile in dieser neuen
Programmiersprache. Es ist möglich kleine Teile funktional zu schreiben und die
Hauptanwendung in einer anderen Sprache, die .NET kompatibel ist. Mit der
zunehmenden Ausbreitung von .NET auch auf andere Betriebssysteme werden die
Einsatzmöglichkeiten von F # natürlich auch vermehrt.
Aber reines Haskell lässt sich am besten in C integrieren und das ist nun mal auf
allen Betriebssystemen vorhanden. F # hat hier den Vorteil der Bequemlichkeit und
der Integration in Visual Studio. Rein qualitativ sind sich die Sprachen wohl gleichauf,
aber Haskell hat eine größere Anhängerschaft. Wie sich das in den nächsten Jahren
ändenr wird, bleibt abzuwarten.
6. Quellen
MSDN
http://msdn.microsoft.com/en-us/fsharp/default
Wikipedia
http://de.wikipedia.org/wiki/F-Sharp
Blog
http://www.fsharp.it/
Lexikon-Eintrag
http://www.itwissen.info/definition/lexikon/F-Sharp-F-sharp-F-sharp.html
Vergleich mehrerer Sprachen
http://www.scriptol.com/programming/fibonacci.php
64
Herunterladen