EMZETT.
Login

Kurz: Haskell-Funktionen sind rein: Gleiche Eingabe, gleiche Ausgabe, keine Nebenwirkungen. Ein- und Ausgabe (Konsole, Dateien, Zufall, Zeit) steht deshalb im Typ.

Teil des Kurses Haskell

Kapitel 7 von 9 im Kurs Haskell (Abschnitt „Praxis“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Reinheit und IO

Haskell-Funktionen sind rein: Gleiche Eingabe, gleiche Ausgabe, keine Nebenwirkungen. Ein- und Ausgabe (Konsole, Dateien, Zufall, Zeit) steht deshalb im Typ: IO a ist eine Beschreibung einer Aktion, die einen Wert vom Typ a liefert. Aktionen setzt man in do-Blöcken zusammen.

import Data.IORef
import Control.Monad (forM_, when, unless, replicateM_, forM)
 
main :: IO ()
main = do
  putStrLn "Start"
  let x = 5 * 5               -- let: reiner Wert
  r <- return (x + 1)          -- <-: Ergebnis einer Aktion binden
  print r
 
  forM_ [1 .. 3] $ \i -> putStrLn ("Runde " ++ show i)
  when (x > 10) $ putStrLn "x ist groß"
  unless (x > 100) $ putStrLn "x ist nicht riesig"
  replicateM_ 2 (putStr "ho ")
  putStrLn ""
 
  zaehler <- newIORef (0 :: Int)           -- veränderlicher Zustand, explizit in IO
  forM_ [1 .. 5] $ \i -> modifyIORef zaehler (+ i)
  readIORef zaehler >>= print
 
  quadrate <- forM [1 .. 4] (\i -> return (i * i))
  print quadrate
 
  mapM_ print (zip [1 :: Int ..] "abc")
  sequence_ [putStr "a", putStr "b", putStrLn "c"]
  let aktion = putStrLn "Aktionen sind Werte"
  aktion >> aktion

Ausgabe:

Start
26
Runde 1
Runde 2
Runde 3
x ist gro?
x ist nicht riesig
ho ho
15
[1,4,9,16]
(1,'a')
(2,'b')
(3,'c')
abc
Aktionen sind Werte
Aktionen sind Werte

Ein wichtiger Gedanke: IO-Code kann reinen Code aufrufen, aber nie umgekehrt (ohne unsafePerformIO). So bleibt der größte Teil des Programms rein, testbar und leicht zu verstehen; die Nebenwirkungen liegen “am Rand”.

Eingabe und Ausgabe

import System.IO
import Text.Read (readMaybe)
 
verarbeite :: String -> String
verarbeite eingabe = unlines (map ausgabeZeile (zip [1 :: Int ..] (lines eingabe)))
  where ausgabeZeile (n, z) = show n ++ ": " ++ reverse z
 
main :: IO ()
main = do
  hSetBuffering stdout NoBuffering
  -- Interaktiv wäre: name <- getLine; contents <- getContents; interact verarbeite
  putStr (verarbeite "abc\nhallo\n")
  hPutStrLn stderr "(das geht auf den Fehlerkanal)"
  print (readMaybe "12" :: Maybe Int)
  let eingabe = "3 4 5"
      zahlen = map read (words eingabe) :: [Int]
  putStrLn ("Summe: " ++ show (sum zahlen))
  writeFile "/tmp/haskell_demo.txt" "Zeile 1\nZeile 2\n"
  appendFile "/tmp/haskell_demo.txt" "Zeile 3\n"
  inhalt <- readFile "/tmp/haskell_demo.txt"
  putStrLn (show (length (lines inhalt)) ++ " Zeilen")
  withFile "/tmp/haskell_demo.txt" ReadMode $ \h -> do
    erste <- hGetLine h
    putStrLn erste

Ausgabe:

1: cba
2: ollah
Just 12
Summe: 12
3 Zeilen
Zeile 1

Ausführlich: getLine (eine Zeile), getContents (alles), interact f (stdin durch eine Funktion schicken), readFile/writeFile, System.Environment (getArgs, lookupEnv), System.Exit (exitWith, ExitFailure).

Fehler in IO

import Control.Exception
import System.IO.Error (isDoesNotExistError)
 
main :: IO ()
main = do
  r1 <- try (evaluate (1 `div` 0 :: Int)) :: IO (Either ArithException Int)
  print r1
  r2 <- try (readFile "/gibt/es/nicht") :: IO (Either IOException String)
  putStrLn (either (const "Datei fehlt") id r2)
  r3 <- try (evaluate (head ([] :: [Int])))
  case r3 of
    Left (e :: SomeException) -> putStrLn "head auf leerer Liste abgefangen"
    Right v -> print v
  (putStrLn "Arbeit" >> throwIO (userError "kaputt")) `catch` \e -> putStrLn ("Gefangen: " ++ show (e :: IOException))
  (putStrLn "mit finally" `finally` putStrLn "Aufräumen")
  r4 <- try (readFile "/gibt/es/nicht")
  case r4 of
    Left e | isDoesNotExistError e -> putStrLn "nicht vorhanden"
    Left _ -> putStrLn "anderer Fehler"
    Right _ -> putStrLn "ok"

Ausgabe:

Left divide by zero
Datei fehlt
head auf leerer Liste abgefangen
Arbeit
Gefangen: user error (kaputt)
mit finally
Aufr?umen
nicht vorhanden

Bevorzuge Maybe/Either für erwartbare Fehler in reinem Code und Exceptions nur für IO-Probleme.

Zustand in reinem Code: die State-Monade

Statt veränderlicher Variablen führt man den Zustand als Parameter. Die State-Monade (Paket mtl) macht das bequem:

import Control.Monad.State
 
zaehle :: State Int Int
zaehle = do
  n <- get
  put (n + 1)
  modify (* 2)
  gets (+ 100)
 
stapel :: State [Int] ()
stapel = do
  modify (1 :)
  modify (2 :)
  modify (3 :)
  modify tail
 
labeln :: [String] -> State Int [(Int, String)]
labeln = mapM (\s -> do { n <- get; put (n + 1); return (n, s) })
 
main :: IO ()
main = do
  print (runState zaehle 5)
  print (execState stapel [])
  print (evalState (labeln ["a", "b", "c"]) 10)
  print (evalState (replicateM 4 (state (\s -> (s * s, s + 1)))) 1)

Ausgabe:

(112,12)
[2,1]
[(10,"a"),(11,"b"),(12,"c")]
[1,4,9,16]

Eigener kleiner Parser mit Applicative

import Data.Char (isDigit, isSpace)
 
newtype Parser a = Parser { runParser :: String -> Maybe (a, String) }
 
instance Functor Parser where
  fmap f (Parser p) = Parser $ \s -> case p s of
    Nothing -> Nothing
    Just (a, rest) -> Just (f a, rest)
 
instance Applicative Parser where
  pure a = Parser $ \s -> Just (a, s)
  Parser pf <*> Parser pa = Parser $ \s -> case pf s of
    Nothing -> Nothing
    Just (f, rest) -> case pa rest of
      Nothing -> Nothing
      Just (a, rest') -> Just (f a, rest')
 
instance Monad Parser where
  Parser p >>= f = Parser $ \s -> case p s of
    Nothing -> Nothing
    Just (a, rest) -> runParser (f a) rest
 
zeichen :: (Char -> Bool) -> Parser Char
zeichen ok = Parser $ \s -> case s of
  (c : cs) | ok c -> Just (c, cs)
  _ -> Nothing
 
viele :: Parser a -> Parser [a]
viele p = Parser $ \s -> case runParser p s of
  Nothing -> Just ([], s)
  Just (a, rest) -> case runParser (viele p) rest of
    Just (as, rest') -> Just (a : as, rest')
    Nothing -> Just ([a], rest)
 
zahl :: Parser Int
zahl = read <$> ((:) <$> zeichen isDigit <*> viele (zeichen isDigit))
 
leer :: Parser ()
leer = () <$ viele (zeichen isSpace)
 
summe :: Parser Int
summe = do
  a <- zahl
  leer
  _ <- zeichen (== '+')
  leer
  b <- zahl
  return (a + b)
 
main :: IO ()
main = do
  print (runParser zahl "123abc")
  print (runParser zahl "abc")
  print (runParser summe "12 + 30 Rest")
  print (fst <$> runParser summe "7+x")

Ausgabe:

Just (123,"abc")
Nothing
Just (42," Rest")
Nothing

Merke

  • IO a beschreibt Aktionen; do-Blöcke setzen sie zusammen, <- bindet Ergebnisse
  • Reiner Code kann keinen IO-Code aufrufen: Nebenwirkungen bleiben am Rand
  • forM_, mapM_, when, unless, IORef für Schleifen und Zustand in IO
  • Fehler: try, catch, evaluate, finally; im reinen Code Maybe/Either
  • State führt Zustand ohne Variablen; Parser lassen sich aus Functor/Applicative/Monad bauen

Übungsaufgabe

Schreibe ein Programm, das Zeilen von stdin liest und ihre Längen ausgibt (interact).

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Maybe, Either und Funktoren

Weiter: Maps, Sets, Text und Module

Alle Kapitel: Haskell im Überblick