Haskell: eine hervorragende prozedurale Sprache
(entropicthoughts.com)Nebeneffekte als Werte erster Klasse behandeln
- In Haskell werden Nebeneffekte, etwa das Erzeugen von Zufallszahlen oder Ausgaben, wie „Werte erster Klasse“ behandelt
- Das heißt: Ein Funktionsaufruf, der einen Nebeneffekt erzeugt, etwa
randomRIO(1, 6), liefert nicht direkt ein Ergebnis zurück, sondern ein Objekt, das eine „Aktion beschreibt, die irgendwann ausgeführt wird“ - Dieses Objekt erzeugt beim tatsächlichen Ausführen einen Zufallswert, davor enthält es jedoch nur den Ausführungsplan
- Ein Typ wie
IO Intsteht für „eine Aktion, die beim Ausführen ein Int erzeugt“; sie wird beim Aufruf nicht sofort ausgeführt, sondern erst dann, wenn sie später benötigt wird - Aufgrund dieser Eigenschaft kann Haskell Nebeneffekte kombinieren und erst später tatsächlich ausführen, anders als traditionelle prozedurale Sprachen, in denen „Funktionsaufruf = sofortige Ausführung“ gilt
De-mystifying do blocks
- Ein
do-Block ist keine magische Syntax, sondern besteht im Wesentlichen aus zwei Operatoren, die Nebeneffekte verknüpfen (bind) und der Reihe nach ausführen (then)
then
- Der Operator
*>führt den Nebeneffekt auf der linken Seite aus, verwirft dessen Ergebnis und führt anschließend den rechten Nebeneffekt aus - Zum Beispiel erzeugt
putStr "hello" *> putStrLn "world"eine einzelneIO ()-Aktion, die beide Ausgaben der Reihe nach kombiniert - Wenn man in einem
do-Block mehrere Zeilen schreibt, wird intern genau so ein Operator für die sequentielle Ausführung verwendet
bind
- Der Operator
>>=führt den Nebeneffekt auf der linken Seite aus und übergibt den erhaltenen Wert an die Funktion auf der rechten Seite - Beispiel:
randomRIO(1, 6) >>= print_sideerzeugt einen Nebeneffekt, der das Würfelergebnis anprint_sideübergibt und ausgibt - Das Muster
<-in einemdo-Block ist im Grunde nur eine bequeme Schreibweise für diesen Operator
Two operators are all of do blocks
- Letztlich werden
do-Blöcke aus genau diesen beiden Operatoren aufgebaut:*>und>>= - Wegen Lesbarkeit und Komfort wird die
do-Syntax häufig verwendet, aber Haskells Stärken kommen noch besser zur Geltung, wenn man auch reichhaltigere Funktionen zur Kombination von Nebeneffekten nutzt
Functions that operate on side effects
- In der Standardbibliothek gibt es verschiedene Funktionen, mit denen sich Nebeneffekte vielseitiger behandeln lassen
pure
pure xerzeugt „eine Aktion, die den Wert x als Ergebnis liefert, ohne zusätzliche Nebeneffekte“- Beispiel:
loaded_die = pure 4erzeugt einIO Int, das immer 4 zurückgibt
fmap
- In der Form
fmap :: (a -> b) -> IO a -> IO berzeugt es eine Aktion, die auf das Ergebnis eines Nebeneffekts eine reine Funktion anwendet und dadurch einen neuen Ergebniswert erzeugt - Beispiel: Mit
length <$> getEnv "HOME"lässt sich eine Aktion erzeugen, die eine Umgebungsvariable holt und dannlengthauf das Ergebnis anwendet, um dessen Länge zu bestimmen
liftA2, liftA3, …
- Funktionen wie
liftA2undliftA3kombinieren die Ergebnisse mehrerer Nebeneffekte mit einer einzigen reinen Funktion zu einem neuen Nebeneffekt - Beispiel:
liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6))erzeugt einen Nebeneffekt, der die Werte zweier Würfe addiert - Dieselbe Aufgabe lässt sich auch mit einer Kombination aus
<$>und<*>erledigen
Intermission: what’s the point?
- Diese Vorgehensweise mag wie eine einfache Funktion erscheinen, die auch in anderen Sprachen möglich ist, aber in Haskell hat sie den Vorteil, dass man Nebeneffekt-Aktionen jederzeit in Variablen auslagern oder neu kombinieren kann, ohne dass sich Ausführungszeitpunkt oder Ergebnis ändern
- Weil Nebeneffekte unabhängig behandelt werden, gibt es beim Refactoring weniger Verwirrung, und sichere Wiederverwendung auf Basis von equational reasoning wird möglich
sequenceA
sequenceA [IO a] -> IO [a]wandelt „eine Liste von Nebeneffekt-Aktionen“ in „eine einzelne Nebeneffekt-Aktion, die eine Ergebnisliste liefert“ um- Beispiel: Mehrere
log-Aktionen können in einer Liste gesammelt und später mitsequenceAin einem Schritt ausgeführt werden - Auch unendlich wiederholte Nebeneffekte, etwa
repeat (randomRIO(1,6)), lassen sich als Liste speichern, mittake nauf die gewünschte Menge begrenzen und dann mitsequenceAausführen
Interlude: convenience functions
void,sequenceA_,replicateM,replicateM_usw. sind praktisch, wenn man Ergebniswerte nicht nutzt oder Aktionen wiederholt ausführen möchte- Beispiel: Mit
replicateM_ 500 (putStrLn "I will not cheat again.")kann man einen Nebeneffekt vielfach ausführen, ohne die Wiederholungen selbst zu zählen
traverse
traverse :: (a -> IO b) -> [a] -> IO [b]erzeugt eine Aktion, die auf jedes Element einer Liste eine Nebeneffekt-Funktion anwendet und die Ergebnisse wieder in einer Liste sammeltsequenceAist tatsächlich dasselbe wietraverse id, undtraverse_ist die Variante, die die Ergebnisse verwirft
for
-
forhat dieselbe Funktion wietraverse, nimmt die Argumente aber in umgekehrter Reihenfolge entgegen -
Beispiel: In der Form
for numbers $ \n -> ...lässt sich eine Art „for-Schleife“ natürlich ausdrücken -
Durch diese Kombinationsmöglichkeiten lassen sich in Haskell Wiederholungen, Iteration und Umformung von Datenstrukturen, die in anderen Sprachen eigene Syntax brauchen, als Kombination von Bibliotheksfunktionen ausdrücken
Leaning into the first classiness of effects
- Wenn man in Haskell Nebeneffekte aktiv als Werte erster Klasse nutzt, lassen sich Code-Duplikate verringern und Strukturen verbessern
- So kann man zum Beispiel bei einer Logik zur Primfaktorzerlegung großer Zahlen mit Cache statt
IOauchStateverwenden, um eine Struktur zu schaffen, in der „Nebeneffekte existieren, aber keine äußeren Auswirkungen haben“ - Solche strukturierten Nebeneffekte werden nur dort eingesetzt, wo sie nötig sind; der übrige Code kann als reine Funktion erhalten bleiben, was zugleich Sicherheit und Flexibilität bringt
- Am Ende lassen sich die eigentlichen Nebeneffekte mit
evalStateund Ähnlichem ausführen und in reine Werte überführen
Things you never need to care about
- Verschiedene Namen aus älteren Haskell-Zeiten wie
>>,return,mapMusw. lassen sich durch heutige Funktionen wie*>,pure,traverseund ähnliche ersetzen - Sie stammen aus „alten Namen oder monadenzentrierten Entwürfen“, während heute eher ein auf Applicative oder allgemeineren Functoren basierender Ansatz empfohlen wird
Appendix A: Avoiding success and uselessness
- Die Aussage „Haskell avoids success“ bedeutet, dass die Sprache ihre grundlegenden Werte nicht aus Popularität oder Bequemlichkeit opfert
- „Haskell is useless“ verweist auf den Kontext, dass die Sprache anfangs so wirkte, als könne sie wegen vollständig reiner Funktionen nichts Praktisches tun, später aber durch die Behandlung von Nebeneffekten als „Werte erster Klasse“ praktische Nutzbarkeit gewann
Appendix B: Why fmap maps over both side effects and lists
fmaphat eine sehr allgemeine Form,Functor f => (a -> b) -> f a -> f b, und lässt sich daher gleichermaßen auf Listen, Maybe, IO und andere Container- oder Nebeneffekt-Typen anwenden- Auf eine Liste angewendet versieht
fmapalle Elemente mit einer Funktion; aufIOangewendet versieht es den Ergebniswert mit einer Funktion - Solche Strukturen, auf die sich Funktionen anwenden lassen, heißen allgemein Functoren
Appendix C: Foldable and Traversable
Foldableist eine Struktur, deren Elemente man durchlaufen und verarbeiten kannTraversableist eine Struktur, die nicht nur durchlaufen werden kann, sondern sich auch mit neuen Elementen in derselben Form wieder aufbauen lässt- Damit
sequenceAodertraverseWerte einsammeln können, während die ursprüngliche Struktur erhalten bleibt, muss diese StrukturTraversablesein - Bei Datenstrukturen wie Bäumen oder Sets kann die Struktur von den Werten abhängen; deshalb wird unterschieden zwischen Fällen, in denen nur Traversieren möglich ist (
Foldable), und solchen, in denen die Struktur tatsächlich rekonstruiert werden kann (Traversable) - Je nach Bedarf kann man Nebeneffekte flexibel behandeln, etwa indem man erst in eine Liste umwandelt und dann
traverseverwendet
2 Kommentare
Wenn man auf Reddit unterwegs ist, sieht man viele Anzeigen … Aber schon der Name schafft irgendwie eine psychologische Hürde.
Irgendwie wirkt es wie eine ziemlich schwierige und mächtige Sprache …
Hacker-News-Kommentar
Haskells Typsystem ist im Vergleich zu anderen beliebten Sprachen komplex. Insbesondere Operatoren wie
*>,<*>und<*erhöhen über die gesamte Codebasis hinweg die Lernkurve.>>=und>>erneut lernen, um produktiv zu bleiben.Haskell hilft dabei, imperatives Programmieren zu verbessern.
Die verallgemeinerte Version von
traverse/mapMfunktioniert nicht nur für Listen, sondern für alleTraversable-Typen und ist sehr nützlich.traverse :: Applicative f => (a -> f b) -> t a -> f (t b)verwendet werden.Haskell hat mächtige Monaden, was Haskell noch prozeduraler macht.
do-Blöcken kann man Zwischenvariablen verwenden.Zu in Haskell geschriebener Software gehört ImplicitCAD.
Haskell-Code liest sich wie eine prozedurale Sprache, bietet aber die Vorteile beim Arbeiten mit Funktionen mit Seiteneffekten.
>>ist der alte Name von<i>>, und beide Operatoren sind linksassoziativ.>>ist alsinfixl 1definiert und<i>>alsinfixl 4, daher bindet<i>>stärker als>>.Haskells
IO aundakönnen sich ähnlich anfühlen wie asynchron und synchron.In anderen Sprachen kann man einfaches IO mit einer Funktion wie
console.log("abc")ausführen.Wer Haskell noch nicht ausprobiert hat, könnte das echte Haskell mit GHC-Erweiterungen als zu komplex empfinden.