204 lines
5.3 KiB
Markdown
204 lines
5.3 KiB
Markdown
---
|
|
tags:
|
|
- Haskell
|
|
- Code
|
|
- Tutorial
|
|
categories:
|
|
- Haskell
|
|
- Tutorial
|
|
date: 2016-01-01
|
|
title: Monoid? Da war doch was...
|
|
abstract: |
|
|
Monoide tauchen überall auf. Ein Grund sich damit mal etwas eingehender an einen konkreten Beispiel zu beschäftigen.
|
|
---
|
|
|
|
Stellen wir uns vor, dass wir eine Funktion schreiben, die einen String bekommt
|
|
(mehrere Lines mit ACSII-Text) und dieses Wort-für-Wort rückwärts ausgeben soll.
|
|
Das ist ein einfacher Einzeiler:
|
|
|
|
```{ .haskell }
|
|
module Main where
|
|
|
|
import System.Environment (getArgs)
|
|
import Data.Monoid (mconcat)
|
|
import Data.Functor ((<$>))
|
|
|
|
main = do
|
|
ls <- readFile =<< head <$> getArgs
|
|
mconcat <$> mapM (putStrLn . unwords . reverse . words) (lines ls) --die eigentliche Funktion, ls ist das argument.
|
|
```
|
|
|
|
Was passiert hier an Vodoo? Und was machen die ganzen wilden Zeichen da?
|
|
|
|
Gehen wir die Main zeilenweise durch: Wir lesen die Datei, die im ersten
|
|
Kommandozeilen-Argument gegeben wird. getArgs hat folgende Signatur:
|
|
|
|
```haskell
|
|
getArgs :: IO [String]
|
|
```
|
|
|
|
Wir bekommen als eine Liste der Argumente. Wir wollen nur das erste. Also machen
|
|
wir head getArgs. Allerdings fliegt uns dann ein Fehler. head sieht nämlich so
|
|
aus:
|
|
|
|
```haskell
|
|
head :: [a] -> a
|
|
```
|
|
|
|
Irgendwie müssen wird as **in** das IO bekommen. Hierzu gibt es fmap. Somit ist
|
|
|
|
```haskell
|
|
fmap head :: IO [a] -> IO a
|
|
```
|
|
|
|
Ein inline-Alias (um die Funktion links und das Argument rechts zu schreiben und
|
|
sich ne Menge Klammern zu sparen) ist <$>. Somit ist schlussendlich der Inhalt
|
|
der Datei aus dem ersten Argument (lazy) in ls.
|
|
|
|
Eine andere Möglichkeit sich das (in diesem Fall) zu merken, bzw. drauf zu
|
|
kommen ist, dass [] AUCH ein Funktor (sogar eine Monade) ist. Man könnte das
|
|
also auch so schreiben:
|
|
|
|
```haskell
|
|
head :: [] a -> a
|
|
head :: Functor f => [] (f a) -> f a -- das "a" geschickt ersetzt zur Verdeutlichung
|
|
getArgs :: IO [] String
|
|
fmap head :: Functor f => f [] a -> f a
|
|
```
|
|
|
|
fmap "packt" die Funktion quasi 1 Umgebung (Funktor, Monade, ..) weiter rein -
|
|
Sei es nun in Maybe, Either oder irgendwas anderes.
|
|
|
|
Alternatives (ausführliches) Beispiel am Ende.
|
|
|
|
Wenn wir uns die Signatur ansehen, dann haben wir nun
|
|
|
|
```haskell
|
|
head <$> getArgs :: IO String
|
|
```
|
|
|
|
readFile will aber nun ein String haben. Man kann nun
|
|
|
|
```haskell
|
|
f <- head <$> getArgs
|
|
ls <- readFile f
|
|
```
|
|
|
|
kann man auch "inline" mit =<< die Sachen "auspacken".
|
|
|
|
Die 2. Zeile lesen wir nun einfach "von hinten", wie man das meistens tun
|
|
sollte. Hier ist ein
|
|
|
|
```haskell
|
|
lines ls :: [String]
|
|
```
|
|
|
|
was uns den Inhalt der Datei zeilenweise gibt. Mit jeder Zeile möchten wir nun
|
|
folgendes machen:
|
|
|
|
1. nach Wörtern trennen (words)
|
|
2. Wörter in der reihenfolge umkehren (reverse)
|
|
3. Wörter wider zu einer Zeile zusammensetzen (unwords)
|
|
4. diese Zeile ausgeben (putStrLn)
|
|
|
|
Wenn wir uns die Signatur ansehen:
|
|
|
|
```haskell
|
|
(putStrLn . unwords . reverse . words) :: String -> IO ()
|
|
```
|
|
|
|
Das mag im ersten Moment verwirren, daher noch die Signaturen der
|
|
Einzelfunktionen:
|
|
|
|
```haskell
|
|
words :: String -> [String]
|
|
reverse :: [a] -> [a]
|
|
unwords :: [String] -> String
|
|
putStrLn :: String -> IO ()
|
|
```
|
|
|
|
Da wir am Ende in der IO-Monade landen müssen wir das auf unsere Zeilen mit mapM
|
|
statt map anwenden. Dies sorgt auch dafür, dass die Liste der reihe nach
|
|
durchgegangen wird. mapM mit unserer Funktion schaut dann so aus:
|
|
|
|
```haskell
|
|
mapM (putStrLn . unwords . reverse . words) :: [String] -> [IO ()]
|
|
```
|
|
|
|
eek! Das [IO ()] sieht ekelig aus. Wir haben eine Liste von IO-gar nichts. Das
|
|
können wir eigentlich entsorgen. Da wir innerhalb der main-Funktion in einer
|
|
IO-Monade sind, wollen wir IO () anstatt [IO ()] zurück haben.
|
|
|
|
Wenn wir uns jetzt erinnern, dass [] auch nur eine Monade ist und dass jede
|
|
Monade ein Monoid ist, dann ist die Lösung einfach. Monoide haben eine
|
|
"append"-funktion (mappend oder (<>) genannt). Wenn wir "nichts" an "nichts"
|
|
anhängen, dann erhalten wir .... _Trommelwirbel_ "nichts"! Wir müssen die [IO
|
|
()]-Liste also "nur noch" mit mappend falten. Hierzu gibt es schon eine
|
|
vorgefertigte Funktion:
|
|
|
|
```haskell
|
|
mconcat :: [a] -> a
|
|
mconcat = foldr mappend mempty
|
|
```
|
|
|
|
Was genau die gewünschte Faltung macht. Wir müssen nun wieder fmap nehmen, da
|
|
wir die Liste selbst falten wollen - und nicht map, welches auf den IO ()
|
|
innerhalb der Liste arbeiten würde. Durch die Faltung fällt die Liste nun auf IO
|
|
() zusammen.
|
|
|
|
Viel Voodoo in wenig Code, aber wenn man sich dran gewöhnt hat, sind Monaden in
|
|
Monaden auch nicht schlimm. Man muss sich immer nur richtig "rein" fmap'en.
|
|
|
|
---
|
|
|
|
Kleinen Tipp gab es noch: mapM\_ macht genau das, was oben mit mconcat erreicht
|
|
werden sollte. Somit kann man auch
|
|
|
|
```haskell
|
|
mapM_ (putStrLn . unwords . reverse . words) (lines ls)
|
|
```
|
|
|
|
schreiben. Ich hab es aber mal wegen der klarheit oben so gelassen.
|
|
|
|
## Alternatives fmap-Beispiel
|
|
|
|
Nehmen wir als alternatives Beispiel mal an:
|
|
|
|
```haskell
|
|
a :: IO Maybe State t
|
|
```
|
|
|
|
Um Funktionen vom Typ
|
|
|
|
```haskell
|
|
f :: IO a -> IO a
|
|
f a -- valide
|
|
```
|
|
|
|
zu nehmen, brauchen wir nichts machen. Bei
|
|
|
|
```haskell
|
|
f' :: Maybe a -> Maybe a
|
|
```
|
|
|
|
brauchen wir 1 fmap, also ein
|
|
|
|
```haskell
|
|
f' a -- error
|
|
f' <$> a
|
|
```
|
|
|
|
um eine Funktion
|
|
|
|
```haskell
|
|
f'' :: State t -> State t
|
|
```
|
|
|
|
zu benutzen folglich:
|
|
|
|
```haskell
|
|
f'' a -- error
|
|
f'' <$> a -- error
|
|
fmap f'' <$> a
|
|
```
|