Wie funktioniert Haskell? Wie unterscheidet es sich von anderen Programmiersprachen?
Hier ein sehr einfaches Projekt in der Sprache: mankinskin/HaskellGame
Der Knackpunkt ist, dass es in Haskell eine klare Unterscheidung zwischen reinen Funktionen und Funktionen mit Seiteneffekt gibt. Normalerweise sind alle Funktionen in Haskell "rein", das heißt sie werden bei jeder Ausführung mit den selben Argumenten garantiert das selbe Ergebnis liefern. Du kannst dir solche Funktionen wie eine Tabelle vorstellen, die Eingabewerte auf feste Ausgabewerte abbildet. Das vereinfach das Programmieren extrem, weil du dir keine Gedanken über den Kontext machen musst, in dem du eine reine Funktion ausführst. Sie ist quasi komplett unabhängig.
Nur reicht das nicht um Programme zu schreiben, die auf unvorhersehbare Zustände reagieren können sollen. Es gibt Funktionen, die mit dem Zustand außerhalb des Programms interagieren sollen. Zb. soll eine Funktion den Zustand einer Taste auf der Tastatur lesen können, oder einen Text ausdrucken können. Diese Funktionen sind aber nicht mehr unabhängig und können bei jeder Ausführung andere Ergebnisse liefern, eben entweder "gedrückt"/"nicht gedrückt" oder "Fehler beim Drucken: Tinte leer". Diese Funktionen haben praktisch die "echte Welt" als Argument, und sind nicht vollständig in der Sprache gekapselt. Sie sind nicht rein funktional und können Effekte außerhalb des Programms erzeugen.
Um mit der externen Welt interagieren zu können, ermöglicht Haskell es daher auch, Funktionen mit Seiteneffekten aufzurufen, welche "Aktionen" genannt werden. Diese Aktionen müssen einen Typen namens IO a verwenden. IO ist hierbei ein Datentyp mit einem Typenparameter a, d.h. IO kann für unterschiedliche andere Typen definiert sein, zB IO String wäre das Ergebnis einer Aktion (d.h. einer Funktion mit Seiteneffekt), die einen String zurückgibt. Wichtig ist, dass dieser Typ nie wieder aus dem IO "Gefängnis" entkommen kann. Bis zum Ende des Programms leben diese Daten in der Welt der Seiteneffekte. Es ist allerdings schon noch möglich, pure Funktionen auf den enthaltenen String aufzurufen, nur wären alle Ergebnisse davon eben auch innerhalb des IO "Gefängnisses". D.h. wir können eine (reine) Funktion
length :: String -> Int 
(Nimmt einen String und gibt eine Int zurück)
auf einen IO String anwenden und ein IO Int erhalten, mittels "bind" und "return".
return :: a -> IO a 
(>>=) :: IO a -> (a -> IO b) -> IO b 
Diese beiden Funktionen machen IO übrigens zu einer Monade
.
return steckt einen Wert mit dem Typen a in das IO Gefängnis. Es gibt keine Möglichkeit ein IO a zu einem a zurückzuformen. Das einzige was wir tun können, ist eine Funktion des Typen a -> IO b auf den Inhalt des IO anzuwenden, mittels bind. Wichtig ist hier wieder, dass diese Funktion ebenfalls etwas in einem IO zurückgeben muss. IO ist praktisch der Kontext der Seiteneffekte, der eben aus Sicherheitsgründen nicht "vergessen" werden darf. Wenn etwas aus einer Funktion mit Seiteneffekt kommt, hängt auch alles was von diesem Wert abgeleitet wird von dieser nicht-reinen Funktion ab.
Wir würden also unsere Funktion length mit return zu einer anderen Funktion g komponieren, damit sie IO Int zurückgibt:
g :: String -> IO Int 
g string = return (length string) 
(ohne Klammern: return . length $ string diese Notation verwendet currying, und macht vieles in Haskell leichter)
Diese Funktion kann jetzt mit "bind" (>>=) verwendet werden. Um also die Länge eines mittels getLine :: IO String eingelesenen Strings zu berechnen:
getLineLen :: IO Int 
getLineLen = getLine >>= return . length 
Wir haben also jetzt eine Funktion, welche von der "externen Welt" anhängig ist, ausgeführt und ihre Daten weiterverwendet. Das hört sich alles sehr kompliziert an, aber man gewöhnt sich schnell daran, und dann sind die Vorteile des funktionalen Programmierens nicht mehr zu übersehen. Denn die meisten Berechnungen sind tatsächlich nicht von Benutzereingaben abhängig, und es ist sinnvoll, die meisten Funktionen rein funktional zu betrachten, denn sie verarbeiten einfach nur Daten in andere Daten. Das macht Haskell Programme sehr gut skalierbar, da sie tendenziell weniger stark gekoppelt sind. Es gibt nämlich keinen Zustand, von dem eine Funktion implizit abhängig ist, sondern alles was relevant ist, steht in den Argumenten. Andernfalls gibt die Funktion IO zurück.