initial
This commit is contained in:
203
Coding/Haskell/Code Snippets/Monoid.md
Normal file
203
Coding/Haskell/Code Snippets/Monoid.md
Normal file
@ -0,0 +1,203 @@
|
||||
---
|
||||
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
|
||||
```
|
Reference in New Issue
Block a user