Kundenrezension

1 von 3 Kunden fanden die folgende Rezension hilfreich
2.0 von 5 Sternen Haskell - intensiv nachbessern bitte!, 31. Januar 2014
Rezension bezieht sich auf: Haskell-Intensivkurs: Ein Kompakter Einstieg in die Funktionale Programmierung (Xpert.press) (German Edition) (Taschenbuch)
Ich habe selbst Erfahrungen mit Haskell-Programmierung und suche ein Buch, das ich Studenten in die Hand drücken kann. Es gibt zahlreiche Lobpreisungen von Haskell in deutscher Sprache in Zeitschriftenartikeln und im Internet, aber häufig schlage ich die Hände über dem Kopf zusammen, weil die vorgestellten Lösungen reichlich umständlich, manchmal sogar falsch sind und die Argumente für Haskell am Kern der Sache vorbeigehen. Hebt sich dieses Buch positiv von anderen Beiträgen ab?

Eine Rezension von Einführung in die Programmierung mit Haskell bemängelt, dass jenes Buch kein Wort über Monaden enthält, während für das vorliegende Buch eine Rezension die Einführung in Monaden lobt. Weil Monade in Haskell mit vielen Irrtümern belastet sind, (siehe: "What a Monad is not" in der Haskell-Wiki) werde ich mir also genau das Kapitel über Monaden genauer anschauen.

Das Kapitel 17 über Monade beginnt auf Seite 210. Auf Seite 211 gibt es bereits den ersten Fehler: In der Implementierung von "h" sind in der Verkettung "gMit ++ fMit" die Operanden vertauscht, die Texte werden also entgegen der chronologischen Reihenfolge zusammengesetzt, was auch der später gezeigten allgemeinen Implementierung des Writer-Monads widerspricht. Auf Seite 212 sieht man in der Ausgabe von "h 5" ein fehlendes Leerzeichen zwischen dem Punkt und "fertig". D.h. den darüber dargestellten Programmteil haben die Autoren offenbar so nicht ausgeführt. Als nächstes werden Zufallszahlen im Modul "Data.Random" verortet, tatsächlich muss man sie aber von "System.Random" importieren.

Auf Seite 215 werden für Abschnitt 17.3.2 durch die Überschrift Umwandlungsregeln für if-then-else innerhalb der do-Notation in Aussicht gestellt. Der Abschnitt zeigt aber keinerlei solche Umwandlung, er weist nur darauf hin, dass die Then- und Else-Zweige ggf. eigene do-Schlüsselwörter benötigen. Das ist aber keine Besonderheit der if-then-else-Syntax, sondern trifft auch auf alle Funktionen zu, beispielsweise "ifThenElse" oder "when". Tatsächlich zeigt das Beispiel aber eine andere Besonderheit:

if p then do
..anweisung2
..anweisung3
else do
..anweisung4
..anweisung5

Wenn in einem do-Block eine Zeile in der ersten Spalte des Blockes beginnt, so wertet der Haskell-Übersetzer dies normalerweise als neue monadische Aktion, und fügt ein (>>) oder (>>=) ein. In dem Beispiel beginnt eine Zeile allerdings mit "else" und dieses wird trotz genannter Regel dem vorangehenden "if" zugeordnet. Dies ist eine syntaktische Ausnahme, um eine Einrückung zu ermöglichen, die viele Programmierer aus imperativen Sprachen gewohnt sind. Siehe Artikel DoAndIfThenElse in der GHC-Wiki.

Persönlich finde ich das überflüssig, weil ich aus Symmetriegründen den Then- und Else-Teil gleich einrücken würde:

if p
..then do
....anweisung2
....anweisung3
..else do
....anweisung4
....anweisung5

Darüber hinaus halte ich eine spezielle Syntax für if-then-else überflüssig, weil es auch die Standardfunktion ifThenElse gibt. Zuletzt weisen die Autoren darauf hin, dass man den Else-Teil nicht weglassen darf. An dieser Stelle könnten sie den Leser darauf hinweisen, dass es die Funktion 'when' gibt, bei der man genau das darf. Meine Meinung: Wenn man gleich die Funktion ifThenElse verwendet, dann ist es klarer, dass der Else-Teil immer dazu gehört, und der Wechsel von einer Funktion "ifThenElse" zu einer Funktion "when" erscheint mir auch leichter zu sein, als ein Wechsel von einer speziellen Syntax (if-then-else) zu einer regulären Funktion (when).

Weiter geht es auf Seite 219 mit dem Reader-Monad. Ich musste mich zuerst durch das Programm arbeiten, um überhaupt zu verstehen, welches Problem die Autoren lösen wollen. Anscheinend betrachten die Autoren "Magenta" als eine Farbnuance von "Rot" und "Cyan" als eine Nuance von "Gruen". Aus irgendeinem Grund wollen sie eine Liste von Texten einlesen, in welchen sich Namen von Grundfarben und Namen von Nuancen abwechseln, und daraus eine Liste machen, in der diese Zuordnung sozusagen maschinenverständlich aufbereitet ist. Was die Motivation für so eine Aufgabe sein könnte, erschließt sich mir zunächst nicht.

Die Implementierung ist ebenso merkwürdig wie die Aufgabenstellung. Mir sticht sofort ins Auge, dass die Funktion liesFarben erstens nicht total ist, sie zweitens zwei Funktionen "rot" und "gruen" aufruft, die nahezu gleich sind, und drittens sich viel einfacher mit einem mapM implementieren lässt. Ersteres bedeutet, dass der Funktionswert undefiniert ist, wenn eine andere Farbe als "Rot" und "Gruen" gelesen wird. Wenn man sich Warnungen ausgeben lässt, so warnt ein Haskell-Übersetzer in der Regel vor unvollständigen Fallunterscheidungen wie dieser. Auf diese Problematik sollte der Text wenigstens hinweisen. Zweiteres heißt, dass ohne Not redundanter Programmtext geschrieben wird. Beide Funktionen unterscheiden sich nur darin, welcher Farbe-Konstruktor verwendet wird. Man hätte es viel einfacher haben können, indem man "rot" und "gruen" zusammenfasst zu

sucheFarbe :: [String] -> Reader Woerterbuch (Int, [String])

und dann die Konstruktoren Rot und Gruen erst in den Fallbehandlungen von liesFarben anwendet. Aber auch das ist noch sehr umständlich. Das "rest"-Argument in "rot" und "gruen" wird nämlich gar nicht verarbeitet, sondern nur durchgereicht. Also würde es auch ein

sucheFarbe :: String -> Reader Woerterbuch Int

tun. Eine Fallbehandlung von liesFarben sieht dann nur noch so aus:

liesFarben ("Rot":nuance:rest) = do
..nuanceInt <- sucheFarbe nuance
..erg <- liesFarben rest
..return (Rot nuanceInt : erg)

Aber auch das ist alles noch sehr umständlich, weil drittens eigentlich ein mapM geboten ist. Ich würde so vorgehen: Zuerst schreibe ich eine Funktion, welche eine Liste in Paare benachbarter Elemente zerlegt:

pairs :: [a] -> [(a,a)]

Dann schreibe ich eine Funktion liesFarbe, die sich mit genau einer Farbe und ihrer Nuance beschäftigt:

liesFarbe :: (String, String) -> Reader Woerterbuch Farbe
liesFarbe (farbe, nuance) =
..fmap (case farbe of "Rot" -> Rot; "Gruen" -> Gruen) $
..sucheFarbe nuance

Zum Schluss setze ich zusammen:

liesFarben = mapM liesFarbe . pairs

Die Autoren entschuldigen sich am Ende des Abschnittes mit dem Satz:
"Da wir unser Beispiel einfach gehalten haben, um die Konzepte zu verdeutlichen, erfordert es ein wenig Phantasie, den Vorteil der Verwendung der Monade zu sehen."

Wenn sie ihr Beispiel wirklich einfach gehalten hätten, so wie ich es gezeigt habe, dann hätten sie auch die Vorteile von Monaden demonstrieren können. Im Englischen nennen es die Haskell-Freunde "separation of concerns", zu deutsch etwa: "immer nur eine Sache auf einmal erledigen". Ich denke, ich habe das ganz gut gezeigt: Eine Handvoll einfach zu verstehende Funktionen wie "pairs" und "liesFarbe" lassen sich mit ebenfalls gut verständlichen Kombinatoren wie "mapM" und "." zu etwas Großem zusammensetzen.

Im übrigen sehe ich auch in real verwendeten Haskell-Paketen häufig Code, in dem auf umständliche Weise Funktionen wie "map" oder "mapM" mit Anwendungscode vermengt werden. Es wäre wirklich nötig in einem Lehrbuch mit gutem Beispiel voranzugehen.

Kommen wir zum Zustandsmonad auf Seite 220. Auch hier versuchen die Autoren ein neues Konzept anhand eines unklaren Beispiels einzuführen. Es geht um einen Getränkeautomaten. Aber was soll dieser Getränkeautomat genau tun? Erst im Verlaufe des Programmes beginne ich zu erahnen, dass der Automat nicht mit Euro bezahlt wird, sondern man eine Geldeinheit pro Flasche entrichten muss. Diese Vereinfachung finde ich sinnvoll, aber keineswegs selbstverständlich. Ein Hinweis darauf muss mir entgangen sein. Der Zustandsgraph in Abbildung 17.1 zeigt drei Zustände 0, 1, 2, welche im Datentyp Automat als Int deklariert sind, bei denen mir aber nicht klar ist, was sie bedeuten. Die Autoren wären gut beraten, für die Zustände einen Aufzählungstypen mit aussagekräftigen Namen zu verwenden, ähnlich dem Typ "Aktion". Auch beim Typ "Aktion" ist mir allerdings nicht klar, was die Aktionen "Flasche" und "Entnehmen" bedeuten. Bei der Aktion "Geld" tippe ich auf den Einwurf einer Geldeinheit. Im Programm ist es möglich mit den Aktionen "Geld" und "Entnehmen" vom Zustand 2 in den Zustand 1 zu gelangen. Diese Kante fehlt in der Abbildung. Mich beschleicht der Verdacht, dass das Programm eigentlich gar keiner Unterscheidung von Zuständen bedarf. Kann man nicht einfach zwei Zähler verwenden: Einer für die eingeworfene Geldmenge und einer für den Umfang des Vorrates an Flaschen? Geldeinwurf erhöht den Geldzähler und Auswurf einer Flasche erniedrigt den Geldzähler und den Vorratszähler. Indem die Autoren die Zustände einfach durchnumeriert haben, haben sie sich schön um eine Klärung ihrer Bedeutung herumgedrückt. Deswegen könnte es gut sein, dass es eigentlich nur einen Zustand gibt, nämlich "Automat funktioniert".

So wirr wie die Aufgabenstellung sieht auch die Implementierung aus. Sie besteht aus der Funktion mit dem Typen

automat :: [Aktion] -> State Automat [String]

und Sie ahnen es schon, auch diese Funktion lässt sich im Wesentlichen als mapM schreiben. Stattdessen vermengen die Autoren die Fallunterscheidung zum Listenskelett mit der Fallunterscheidung zur Aktion, wobei die Aktionen "Geld" und "Flaschen" mit eigenen Funktionen implementiert sind, aber "Entnehmen" direkt in "automat" behandelt wird. Das Hin- und Herspringen zwischen "automat", "geld" und "flaschen" erinnert mich stark an das lange überwunden geglaubte GOTO. Beim Fall "Entnehmen" habe ich wieder zu bekritteln, dass mit "error" die Funktion nicht total ist und weiterhin wundere ich mich über das umständliche "case (guthaben z) of n | n==0 -> ..." welches man doch eher als "case guthaben z of 0 -> ..." schreiben würde.

Zum Schluss scheitert man daran, das Beispiel auszuführen, denn auf Seite 222 ist die Hugs-Eingabezeile so formatiert, dass man das zweite Argument von "runState" für die Ausgabe von Hugs halten muss. Die Einrückung für "Entnehmen" ist nett gemeint, aber Anfänger bekommen leicht den Eindruck, dass man das so einrücken müsste, insbesondere weil man ihnen zuvor gesagt hat, dass Einrückungen in Haskell eine Bedeutung haben. Es wäre sicher sinnvoller gewesen, zur Erzeugung eines Anfangszustandes eine Funktion zu programmieren, dann hätte der gesamte runState-Aufruf in zwei Eingabezeilen gepasst. Im übrigen hatte ich hinter dem Namen "automat" eher eine Funktion erwartet, die einen Automat-Datenverbund erzeugt. Die im Buch gezeigte Funktion hätte ich eher "ausfuehren" genannt.

Als letztes kommt der Listenmonad ab Seite 222. Dort wird eine Funktion vorgestellt, die alle Permutationen einer Liste aufzählt - selbstverständlich mit dem typischen Anfängerfehler, die Funktion "delete" zu verwenden. In der Haskell-Wiki wird unter "Haskell programming tips" fast genau dieses Beispiel behandelt und die Funktion "removeEach" vorgeschlagen, mit welcher man Permutationen ohne die Typbeschränkung "Eq" berechnen kann. Überdies funktioniert "removeEach" auch beim Auftreten von Elementen, die bezüglich (==) gleich sind.

Die Funktion slowSort ließe sich sehr knapp implementieren als "filter istSortiert . perm". Die Autoren ziehen jedoch eine mehrzeilige Lösung im Listenmonad vor, weil nunmal der Listenmonad gerade das Thema ist.

Das zum Thema Monade. Ansonsten habe ich zu bemängeln, dass in dem Buch Programmierfehler und Ausnahmebehandlung durcheinander geworfen werden. Wenn beispielsweise dem Getränkeautomaten die Flaschen ausgehen, dann darf natürlich nicht das Programm abstürzen, so wie es die Autoren programmiert haben. Vielmehr muss das Programm auf diese Situation vorbereitet sein und dem Benutzer eine sinnvolle Fehlermeldung mitteilen, ihm das Geld zurückgeben und am besten gleich den Getränkelieferer informieren. Auch die Verwendung des Writer-Monads für Debug-Ausgaben ist nicht korrekt. Für die Fehlersuche kann man Hilfsausgaben vorübergehend(!) mit Debug.Trace.trace ausgeben, aber das Programm dauerhaft auf die Fehlersuche anzupassen, ist sicher nicht der richtige Weg. Eine sinnvolle Anwendung für ein Protokoll wäre eine Log-Datei für einen Web-Server, in der der Web-Server gestellte Anfragen archiviert.

Weiterhin stört mich, dass aus dem Buch nur schwer zu erkennen ist, welche Programmschnippsel zusammengehören. Es werden regelmäßig Bezeichner an einer Stelle definiert und an anderer Stelle verwendet, aber der Sichtbarkeitsbereich ist ebensowenig ersichtlich, wie der Satz an importierten Modulen. Eine sinnvolle Einteilung wäre etwa ein Modul pro Kapitel. Wenn man die Dateien jedoch von der Seite der Autoren herunterlädt, so erhält man die Schnipsel genauso aufgeteilt, wie sie in den Blöcken im Buch erscheinen. In dieser Form lassen sie sich nicht in einen Haskell-Interpreter laden! Es ist also gar nicht klar, ob die Autoren ihre Programme überhaupt jemals ausprobiert haben. Meine oben gezeigten Beispiel wecken Zweifel daran. Dieser Umstand erklärt wohl auch, warum sich manchmal Bezeichner von einem Block zum nächsten ändern, ohne dass es die Autoren bemerken. Das sieht man zum Beispiel am Typen "Baum" auf Seite 168: Dort steht "x {elems = size x + size y, ..." Die Variable "x" ist vom Typ "Baum", dieser hat aber kein Element namens "elems". Im Übrigen ist "elems" fettgedruckt, sicher weil das LaTeX-Paket "listings" eine entsprechende Funktion im Modul Data.Array kennt.

Der Datentyp "Baum" eignet sich darüber hinaus gut als Beispiel, wie man Datentypen nicht definiert: In der Definition werden 3 Sprachen und 4 Metaphern vermengt. Die Sprachen sind Deutsch (Baum, Knoten, anzahl, inhalt), Latein (nil von nihil), und Englisch (lT und rT vermutlich leftTree und rightTree, die unnötige Abkürzung macht die Sache nur noch schlimmer), und jedes der Wörter Baum, Knoten, Nil und inhalt gehört anscheinend zu einer anderen Metapher, aber zu welcher? Ich denke, wenn man einen Baum als Vorbild wählt, dann sollte man auch bei Bestandteilen eines Baumes bleiben, also Wurzel, Ast, Zweig, Blatt etc. Auch sonst trifft man in dem Buch viele gequälte denglische Ausdrücke wie Traversierung (statt ... ja was wollen sie eigentlich sagen?), Instanz (statt Ausprägung), Funktionskomposition (statt -verkettung), Pattern match (statt Mustertest). Für mich sind Nicht- oder Schlechtübersetzungen häufig ein Zeichen von Nicht-Verstehen und das deckt sich mit meinem Eindruck, den ich allein aus dem Monad-Kapitel mitgenommen habe.

Habe ich schon erwähnt, dass die Haskell-Module zum Buch teilweise Tabulatoren enthalten? Das ist in Haskell problematisch, weil Einrückungen eine Bedeutung haben, und es wird spätestens dann zum Problem, wenn man Texte mit Leerzeichen und Tabulatoren zusammenkopiert.

In meinem Schlusswort möchte ich auf die Frage zurückkommen, ob man das Buch Studenten in die Hand drücken soll oder nicht. Meine Antwort: Ja, man sollte das durchaus tun, denn daraus kann man eine Menge lernen. Erstens lernt man, dass Programmierer ziemlich schlampig sind und dass dies leider auch auf Haskell-Programmierer zutrifft. Zweitens kann man lernen, wie man es nicht macht und warum. Würde man Studenten nur zeigen, wie man diszipliniert programmiert, dann würden sie einem ja nicht glauben, dass Programmierer zu gewissen Schandtaten wirklich bereit sind.

35 Euro ist diese Diskussionsgrundlage allerdings ganz klar nicht wert. Vielleicht bekommen Sie das Buch irgendwo gebraucht.
Helfen Sie anderen Kunden bei der Suche nach den hilfreichsten Rezensionen 
War diese Rezension für Sie hilfreich? Ja Nein

[Kommentar hinzufügen]
Kommentar posten
Verwenden Sie zum Einfügen eines Produktlinks dieses Format: [[ASIN:ASIN Produkt-Name]] (Was ist das?)
Amazon wird diesen Namen mit allen Ihren Beiträgen, einschließlich Rezensionen und Diskussion-Postings, anzeigen. (Weitere Informationen)
Name:
Badge:
Dieses Abzeichen wird Ihnen zugeordnet und erscheint zusammen mit Ihrem Namen.
There was an error. Please try again.
">Hier finden Sie die kompletten Richtlinien.

Offizieller Kommentar

Als Vertreter dieses Produkt können Sie einen offiziellen Kommentar zu dieser Rezension veröffentlichen. Er wird unmittelbar unterhalb der Rezension angezeigt, wo immer diese angezeigt wird.   Weitere Informationen
Der folgende Name und das Abzeichen werden mit diesem Kommentar angezeigt:
Nach dem Anklicken der Schaltfläche "Übermitteln" werden Sie aufgefordert, Ihren öffentlichen Namen zu erstellen, der mit allen Ihren Beiträgen angezeigt wird.

Ist dies Ihr Produkt?

Wenn Sie der Autor, Künstler, Hersteller oder ein offizieller Vertreter dieses Produktes sind, können Sie einen offiziellen Kommentar zu dieser Rezension veröffentlichen. Er wird unmittelbar unterhalb der Rezension angezeigt, wo immer diese angezeigt wird.  Weitere Informationen
Ansonsten können Sie immer noch einen regulären Kommentar zu dieser Rezension veröffentlichen.

Ist dies Ihr Produkt?

Wenn Sie der Autor, Künstler, Hersteller oder ein offizieller Vertreter dieses Produktes sind, können Sie einen offiziellen Kommentar zu dieser Rezension veröffentlichen. Er wird unmittelbar unterhalb der Rezension angezeigt, wo immer diese angezeigt wird.   Weitere Informationen
 
Timeout des Systems

Wir waren konnten nicht überprüfen, ob Sie ein Repräsentant des Produkts sind. Bitte versuchen Sie es später erneut, oder versuchen Sie es jetzt erneut. Ansonsten können Sie einen regulären Kommentar veröffentlichen.

Da Sie zuvor einen offiziellen Kommentar veröffentlicht haben, wird dieser Kommentar im nachstehenden Kommentarbereich angezeigt. Sie haben auch die Möglichkeit, Ihren offiziellen Kommentar zu bearbeiten.   Weitere Informationen
Die maximale Anzahl offizieller Kommentare wurde veröffentlicht. Dieser Kommentar wird im nachstehenden Kommentarbereich angezeigt.   Weitere Informationen
Eingabe des Log-ins
 

Kommentare


Sortieren: Ältester zuerst | Neuester zuerst
1-8 von 8 Diskussionsbeiträgen
Ersteintrag: 02.04.2014 21:14:28 GMT+02:00
In jedem Fachbuch gibt es Fehler. In jedem Fachbuch kann man Beispiele finden, die verbesserungsfähig oder sogar schlecht sind. Wer im Buch bis zu den Monaden gekommen ist, wird so viel Erfahrung in Haskell besitzen, um einige Beispiele aus diesem Kapitel kritisch genug zu sehen.

Wenn man sich schon etwas "aufregen" möchte, dann doch eher über z.B. das Quicksort-Beispiel. Das aber so oder in ähnlicher Form überall zu finden ist, wenn es um die funktionale Programmierung geht. Wer zweimal durch eine Liste geht, um die Teillisten zu erhalten, macht es einmal zu viel. Für solche Fälle (d.h. Liste -> 2 Teillisten) schreibt man sich eine Hilfsfunktion, die einmal durch die Liste geht und die beiden Teillisten als Tupel zurückliefert. Also eine Art superfilter.

Antwort auf einen früheren Beitrag vom 02.04.2014 21:43:44 GMT+02:00
superfilter = partition

Ansonsten bin ich der Meinung, dass das Kapitel über Monade nicht nur vereinzelte Fehler enthält, sondern im Ganzen schlampig gemacht ist, mit Beispielen, die weder motiviert noch erläutert werden, noch selbsterklärend sind, noch ansprechend gelöst. Anscheinend wollten die Autoren mit Gewalt die Standardbeispiele für Monaden abarbeiten und haben keine guten Beispiele gefunden. Ich gebe zu, es ist wirklich schwer, einfache und trotzdem überzeugende Beispiele zu finden, aber das wäre gerade der Mehrwert, den ich von einem Buch erwarte. Andernfalls kann man auch die Online-Dokumentation verschiedener Monad-Bibliotheken studieren.

Antwort auf einen früheren Beitrag vom 07.04.2014 22:34:40 GMT+02:00
So wie partition definiert ist, geht man auch zweimal durch die Liste durch. Das bringt also in Sachen Laufzeit nichts.

Antwort auf einen früheren Beitrag vom 14.04.2014 17:38:39 GMT+02:00
'partition' geht nur einmal durch die Liste.
Hier die Definition aus base-4.7.0.0:Data.List:

partition :: (a -> Bool) -> [a] -> ([a],[a])
partition p xs = foldr (select p) ([],[]) xs

select :: (a -> Bool) -> a -> ([a], [a]) -> ([a], [a])
select p x ~(ts,fs)
| p x = (x:ts,fs)
| otherwise = (ts, x:fs)

Antwort auf einen früheren Beitrag vom 22.05.2014 23:23:11 GMT+02:00
Das stimmt. In der Doku zu Data.List stand leider die langsame Variante.

Veröffentlicht am 24.06.2014 18:09:32 GMT+02:00
Elloline meint:
Kannst du vielleicht ein anderes Buch über Haskell - besonders in Bezug auf Monaden - empfehlen?

Antwort auf einen früheren Beitrag vom 24.06.2014 18:40:56 GMT+02:00
Auf Englisch fand ich den Artikel "Tackling the awkward squad" von Simon Peyton Jones am besten, dazu noch den Haskell-Wiki-Artikel "What a Monad is not". Auf deutsch finde ich das Buch Haskell - Eine Einführung für Objektorientierte seriöser, dort gibt es auch ein Monad-Kapitel. Allerdings stört mich, dass Doberkat den IO-Monad als nicht-funktional bezeichnet.

Antwort auf einen früheren Beitrag vom 24.06.2014 19:53:24 GMT+02:00
Elloline meint:
Super, danke dir für die schnelle Antwort!
‹ Zurück 1 Weiter ›