Hier die Zusammenfassung des ersten Kapitels von "Real World Haskell" und ein paar Diskussionsergebnissen dazu:
Typen definieren welche Bits & Bytes wie verwendet werden sollen.
In Haskell haben alle Datentypen (wie Puzzelteile) unterschiedliche Formen und Kompiler stellt sicher, dass diese
korrekt zusammen passen, während in dynamischen Programmiersprachen alle Daten einförmig (quasi 'quadratisch') vorliegen und somit immer zu passen scheinen. Doch dann muss via Tests noch mal extra geprüft werden ob ein korrektes Gesamtbild heraus kommt.
Es werden Klammern nur für verschachtelte Funktionsaufrufe benötigt,
da Funktionen einen höheren Prezedenswert als operatoren haben.
Listen erwarten keinen konkreten Datentyp
ghci> :type []
[] :: [a]
'a' ist eine Typenvariable und steht stellvertretend für alle denkbaren
Datentypen, die in einer Liste abgelegt werden können. In einer Liste
kann nur ein Datentyp vorkommen.
Tupel erwarten auch keinen konkreten Datentyp und können obendrein verschiedene
Datentypen aufnehmen:
ghci> (1, 'a')
(1,'a')
ghci> :type it
it :: (Integer, Char)
Die Kombinationsmöglichkeit von Datentypen in Tupeln erlauben
das zusammenlegen zusammengehöriger Daten. Beispiel:
ghci> let books = [("Real World Haskell", 2010), ("Structure and Interpretation of Computer Programs", 1984)]
ghci> :type books
books :: [([Char], Integer)]
ghci> :type False
False :: Bool
ghci> :type (["foo", "bar"], 'a')
(["foo", "bar"], 'a') :: ( [[Char]], Char)
ghci> :type [(True, []), (False, [['a']])]
[(True, []), (False, [['a']])] :: [(Bool, [[Char]])]
Erst durch dem zweiten Eintrag in der Liste wird klar, dass das zweite Element des Tupels vom Typ Char sein muß.
:type lines
lines :: String -> [String]
Warum kommt nicht lines :: [Char] -> [[Char]] heraus?
Wo ist String definiert? Wie kann ich es verwenden?
String ist ein Alias für [Char]. Er ist im Prelude mitdefiniert und kann
entsprechend sofort verwendet werden. Allerdings wird er nicht automatisch
zur TypeInferrence herangezogen.
Eine Funktion in Haskell hat per Default keine Seiteneffekte.
Das drückt sich auch in ihrem Typus aus.
Sollte eine Funktion doch in irgend einer Form das Gesamtsystem beeinflussen,
oder vom Status des Gesamtsystems abhängen, dann muss sich das in ihrem
Typus wider spiegeln.
Beispiel:
-- Seiteneffekt frei
ghci> :type lines
lines :: String -> [String]
-- Seiteneffekt behaftet
ghci> :type readFile
readFile :: IO String
-- Die IO-Monad (hä? Kommt erst noch...) im Typ deutet auf Seiteneffekte hin.
In Haskell wird jede Expression erst zu einem 'thunk' ausgewertet, also einer
Art Platzhalter für das Ergebnis der Expression. Das Ergebnis selbst wird
erst berechnet, sobald der Wert konkret benötigt wird. (nonstrict evaluation)
Dadurch erklärt sich auch, warum wie der Typ des Ausdrucks 1 + 3 zustande kommt.
ghci> :type 1 + 3
1 + 3 :: Num a => a
Num a => sagt: es wird vorausgesetzt, dass es sich bei der Typvariablen a um
einen Typ oder Subtyp des Num-Typ handelt. D.h das Ergebnis des thunks muss vom Typ a
sein.
Zum Vergleich hier mal ein javascript snippet, dass die gleiche Bedeutung hat
wie haskells 1 + 3
function() {
return 1 + 3;
}
Man kann sich also einen thunk ein bisschen wie eine Funktion vorstellen, die keine Parameter
erwartet und die noch nicht ausgeführt wurde. Haskell ist so schlau und weiß, was auch immer da
zurück kommen wird, es muss vom Typ Num sein. Der Wert selbst steht jedoch
erst fest, wenn der thunk ausgeführt wurde.
Was kann last aufgrund seiner Typen-Signatur last :: [a] -> a tun und was
nicht?
Sie kann einen Wert aus der Liste extrahieren
Sie kann Listen nicht zu einem Wert des gleichen Types zusammenfassen (wie z.B. sum)
Sie kann nicht ihren Eingabe-Parameter ignorieren und einen beliebigen Wert des selben Types zurück geben.
Sie kann die Liste vom Typ a nicht in einen anderen Datentyp konvertieren
Sie kann die Liste nicht modifizieren
Wieso kann man das so konkret sagen?
last erwartet eine Liste eines beliebigen
Typs a. Über diesen Typ a ist aber nichts weiter bekannt. Weder ob er von
irgend einem Basistyp wie Num abgeleitet ist noch ob er irgendwelche
Operatoren unterstützt. Daher kann last NUR einen Wert aus der Liste
extrahieren und nichts sonst.
-- lastButOne.hs
lastButOne xs = head(drop(length(xs) -2) xs)
Wenn man diese Funktion dann im GHCi lädt:
ghci> :load lastButOne.hs
[1 of 1] Compiling Main ( lastButOne.hs, interpreted )
Ok, modules loaded: Main.
ghci> lastButOne [1,2,3,4,5]
4
ghci> lastButOne [1,2]
1
ghci> lastButOne [1]
1
--Falsches Ergebnis
ghci> lastButOne []
*** Exception: Prelude.head: empty list
ghci>
Wie soll die Funktion mit eine Liste umgehen, die weniger als zwei Elemenete hat?
Man kann nicht mal nil/null/undefined zurück geben? Selbst ein "Dummywert" wie -1 schließt sich aus, da
lastButOne mit jeder Art von typisierten Liste umgehen können muss.
Mit dem bisherigen Wissen sind mir keine bessren Lösungen eingefallen. Aber im Kapitel 3 werden Pattern-Matching und der Maybe-Basistyp eingeführt, die eine Lösung für dieses Problem darstellen können.