Input and Output

{{% section %}}

Input and Output

@wusyong, 吳昱緯


When a function changes state, we say that the function has side-effects. This chapter explores the boundry of pure and impure.


{{% /section %}}


{{% section %}}

Hello, world!

main = putStrLn "hello, world"  
$ ghc --make helloworld  
[1 of 1] Compiling Main             ( helloworld.hs, helloworld.o )  
Linking helloworld ...
$ ./helloworld  
hello, world

putStrLn

Return an I/O action with result type () (empty tuple)

ghci> :t putStrLn  
putStrLn :: String -> IO ()  
ghci> :t putStrLn "hello, world"  
putStrLn "hello, world" :: IO ()

main action

  • do operation will glue multple IO into one
main = do  
    putStrLn "Hello, what's your name?"  
    name <- getLine  
    putStrLn ("Hey " ++ name ++ ", you rock!")

getLine

name <- getLine: perform the I/O action getLine and then bind its result value to name

ghci> :t getLine  
getLine :: IO String```

---

### tained & untained

This works:
```haskell
main = do  
    putStrLn "Hello, what's your name?"  
    name <- getLine  
    putStrLn $ "Read this carefully, because this is your future: " ++ tellFortune name  

but not this: ``haskell nameTag = "Hello, my name is " ++ getLine -- You can't do name either name = getLine


---

### IO on ghci will still show the result

```haskell
ghci> putStrLn "HEEY"  
HEEY

let binding in do

import Data.Char  
  
main = do  
    putStrLn "What's your first name?"  
    firstName <- getLine  
    putStrLn "What's your last name?"  
    lastName <- getLine  
    let bigFirstName = map toUpper firstName  
        bigLastName = map toUpper lastName  
    putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"

reverseWords

return makes an I/O action out of a pure value (like IO String), it's reverse of <-

main = do   
    line <- getLine  
    if null line  
        then return ()  
        else do  
            putStrLn $ reverseWords line  
            main  
  
reverseWords :: String -> String  
reverseWords = unwords . map reverse . words

putStr

main = do   putStr "Hey, "  
            putStr "I'm "  
            putStrLn "Andy!"
$ runhaskell putstr_test.hs  
Hey, I'm Andy!

putChar

main = do   putChar 't'  
            putChar 'e'  
            putChar 'h'
$ runhaskell putchar_test.hs  
teh

putStr :: String -> IO ()  
putStr [] = return ()  
putStr (x:xs) = do  
    putChar x  
    putStr xs

print

putStrLn . show

main = do   print True  
            print 2  
            print "haha"  
            print 3.2  
            print [3,4,3]
$ runhaskell print_test.hs  
True  
2  
"haha"  
3.2  
[3,4,3]

getChar

main = do     
    c <- getChar  
    if c /= ' '  
        then do  
            putChar c  
            main  
        else return ()
$ runhaskell getchar_test.hs  
hello sir  
hello

when

  • perform do if True
  • return () if False
import Control.Monad   
  
main = do  
    c <- getChar  
    when (c /= ' ') $ do  
        putChar c  
        main

sequence

sequence :: [IO a] -> IO [a]

main = do  
    a <- getLine  
    b <- getLine  
    c <- getLine  
    print [a,b,c]

main = do  
    rs <- sequence [getLine, getLine, getLine]  
    print rs

ghci> sequence (map print [1,2,3,4,5])  
1  
2  
3  
4  
5  
[(),(),(),(),()]

mapM & mapM_

ghci> mapM print [1,2,3]  
1  
2  
3  
[(),(),()]  
ghci> mapM_ print [1,2,3]  
1  
2  
3

forever

import Control.Monad  
import Data.Char  
  
main = forever $ do  
    putStr "Give me some input: "  
    l <- getLine  
    putStrLn $ map toUpper l

forM

import Control.Monad  
  
main = do   
    colors <- forM [1,2,3,4] (\a -> do  
        putStrLn $ "Which color do you associate with the number " ++ show a ++ "?"  
        color <- getLine  
        return color)  
    putStrLn "The colors that you associate with 1, 2, 3 and 4 are: "  
    mapM putStrLn colors

$ runhaskell form_test.hs  
Which color do you associate with the number 1?  
white  
Which color do you associate with the number 2?  
blue  
Which color do you associate with the number 3?  
red  
Which color do you associate with the number 4?  
orange  
The colors that you associate with 1, 2, 3 and 4 are:  
white  
blue  
red  
orange

{{% /section %}}


{{% section %}}

Files and streams

getContents

  • getContents :: IO String
  • lazy I/O, useful for pipeing output
import Data.Char  
  
main = do  
    contents <- getContents  
    putStr (map toUpper contents)

$ cat haiku.txt | ./capslocker  
I'M A LIL' TEAPOT  
WHAT'S WITH THAT AIRPLANE FOOD, HUH?  
IT'S SO SMALL, TASTELESS

Only print lines shorter than 10 characters

main = do  
    contents <- getContents  
    putStr (shortLinesOnly contents)  
  
shortLinesOnly :: String -> String  
shortLinesOnly input =   
    let allLines = lines input  
        shortLines = filter (\line -> length line < 10) allLines  
        result = unlines shortLines  
    in  result

i'm short  
so am i  
i am a loooooooooong line!!!  
yeah i'm long so what hahahaha!!!!!!  
short line  
loooooooooooooooooooooooooooong  
short

$ ghc --make shortlinesonly  
[1 of 1] Compiling Main             ( shortlinesonly.hs, shortlinesonly.o )  
Linking shortlinesonly ...  
$ cat shortlines.txt | ./shortlinesonly  
i'm short  
so am i  
short

interact

main = interact shortLinesOnly  
  
shortLinesOnly :: String -> String  
shortLinesOnly input =   
    let allLines = lines input  
        shortLines = filter (\line -> length line < 10) allLines  
        result = unlines shortLines  
    in  result

main = interact $ unlines . filter ((<10) . length) . lines

tell if line is palindrome

respondPalindromes contents = unlines (map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") (lines contents))  
    where   isPalindrome xs = xs == reverse xs

respondPalindromes = unlines . map (\xs -> if isPalindrome xs then "palindrome" else "not a palindrome") . lines  
    where   isPalindrome xs = xs == reverse xs

main = interact respondPalindromes
$ runhaskell palindromes.hs  
hehe  
not a palindrome  
ABCBA  
palindrome  
cookie  
not a palindrome

Read/write files

import System.IO  
  
main = do  
    handle <- openFile "girlfriend.txt" ReadMode  
    contents <- hGetContents handle  
    putStr contents  
    hClose handle

$ runhaskell girlfriend.hs  
Hey! Hey! You! You!  
I don't like your girlfriend!  
No way! No way!  
I think you need a new one!

openFile

  • openFile :: FilePath -> IOMode -> IO Handle
  • type FilePath = String
  • data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode

hGetContents/hClose/hGetLine/hPutStr/hPutStrLn/hGetChar


withFile

withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a

import System.IO     
    
main = do     
    withFile "girlfriend.txt" ReadMode (\handle -> do  
        contents <- hGetContents handle     
        putStr contents)

withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a  
withFile' path mode f = do  
    handle <- openFile path mode   
    result <- f handle  
    hClose handle  
    return result

readFile

import System.IO  
  
main = do  
    contents <- readFile "girlfriend.txt"  
    putStr contents

writeFile

import System.IO     
import Data.Char  
    
main = do     
    contents <- readFile "girlfriend.txt"     
    writeFile "girlfriendcaps.txt" (map toUpper contents)

$ runhaskell girlfriendtocaps.hs  
$ cat girlfriendcaps.txt  
HEY! HEY! YOU! YOU!  
I DON'T LIKE YOUR GIRLFRIEND!  
NO WAY! NO WAY!  
I THINK YOU NEED A NEW ONE!

appendFile

import System.IO     
    
main = do     
    todoItem <- getLine  
    appendFile "todo.txt" (todoItem ++ "\n")

$ runhaskell appendtodo.hs  
Iron the dishes  
$ runhaskell appendtodo.hs  
Dust the dog  
$ runhaskell appendtodo.hs  
Take salad out of the oven  
$ cat todo.txt  
Iron the dishes  
Dust the dog  
Take salad out of the oven

hSetBuffering

  • BufferMode: NoBuffering, LineBuffering or BlockBuffering (Maybe Int)
main = do   
    withFile "something.txt" ReadMode (\handle -> do  
        hSetBuffering handle $ BlockBuffering (Just 2048)  
        contents <- hGetContents handle  
        putStr contents)

hFlush

  • LineBuffering: flush after each line
  • Block... ...

partially apply type parameters

type IntMap v = Map Int v
-- or
type IntMap = Map Int

Remove words

import System.IO  
import System.Directory  
import Data.List  
  
main = do        
    handle <- openFile "todo.txt" ReadMode  
    (tempName, tempHandle) <- openTempFile "." "temp"  
    contents <- hGetContents handle  
    let todoTasks = lines contents     
        numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks     
    putStrLn "These are your TO-DO items:"  
    putStr $ unlines numberedTasks  
    putStrLn "Which one do you want to delete?"     
    numberString <- getLine     
    let number = read numberString     
        newTodoItems = delete (todoTasks !! number) todoTasks     
    hPutStr tempHandle $ unlines newTodoItems  
    hClose handle  
    hClose tempHandle  
    removeFile "todo.txt"  
    renameFile tempName "todo.txt"

openTempFile

  • takes a dir and then a name
  • getCurrentDirectory

removeFile/renameFile

{{% /section %}}


{{% section %}}

Command line arguments

  • getArgs :: IO [String]
  • getProgName :: IO String
import System.Environment   
import Data.List  
  
main = do  
   args <- getArgs  
   progName <- getProgName  
   putStrLn "The arguments are:"  
   mapM putStrLn args  
   putStrLn "The program name is:"  
   putStrLn progName

$ ./arg-test first second w00t "multi word arg"  
The arguments are:  
first  
second  
w00t  
multi word arg  
The program name is:  
arg-test

todolist features

import System.Environment   
import System.Directory  
import System.IO  
import Data.List  
  
dispatch :: [(String, [String] -> IO ())]  
dispatch =  [ ("add", add)  
            , ("view", view)  
            , ("remove", remove)  
            ]

main

main = do  
    (command:args) <- getArgs  
    let (Just action) = lookup command dispatch  
    action args

add/view/remove

add :: [String] -> IO ()  
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")

view :: [String] -> IO ()  
view [fileName] = do  
    contents <- readFile fileName  
    let todoTasks = lines contents  
        numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks  
    putStr $ unlines numberedTasks

remove :: [String] -> IO ()  
remove [fileName, numberString] = do  
    handle <- openFile fileName ReadMode  
    (tempName, tempHandle) <- openTempFile "." "temp"  
    contents <- hGetContents handle  
    let number = read numberString  
        todoTasks = lines contents  
        newTodoItems = delete (todoTasks !! number) todoTasks  
    hPutStr tempHandle $ unlines newTodoItems  
    hClose handle  
    hClose tempHandle  
    removeFile fileName  
    renameFile tempName fileName

Result

$ ./todo view todo.txt  
0 - Iron the dishes  
1 - Dust the dog  
2 - Take salad out of the oven  
  
$ ./todo add todo.txt "Pick up children from drycleaners"  
  
$ ./todo view todo.txt  
0 - Iron the dishes  
1 - Dust the dog  
2 - Take salad out of the oven  
3 - Pick up children from drycleaners  
  
$ ./todo remove todo.txt 2  
  
$ ./todo view todo.txt  
0 - Iron the dishes  
1 - Dust the dog  
2 - Pick up children from drycleaners

{{% /section %}}


{{% section %}}

Randomness


random

  • random :: (RandomGen g, Random a) => g -> (a, g)
  • RandomGen typeclass for source of randomness
  • Random typeclass for things that can take on random values
  • StdGen is instance of RandomGen
  • mkStdGen :: Int -> StdGen
ghci> random (mkStdGen 100) -- wrong

ghci> random (mkStdGen 100) :: (Int, StdGen)  
(-1352021624,651872571 1655838864)

ghci> random (mkStdGen 949488) :: (Float, StdGen)  
(0.8938442,1597344447 1655838864)  
ghci> random (mkStdGen 949488) :: (Bool, StdGen)  
(False,1485632275 40692)  
ghci> random (mkStdGen 949488) :: (Integer, StdGen)  
(1691547873,1597344447 1655838864)

Example: coin toss

threeCoins :: StdGen -> (Bool, Bool, Bool)  
threeCoins gen =   
    let (firstCoin, newGen) = random gen  
        (secondCoin, newGen') = random newGen  
        (thirdCoin, newGen'') = random newGen'  
    in  (firstCoin, secondCoin, thirdCoin)

ghci> threeCoins (mkStdGen 21)  
(True,True,True)  
ghci> threeCoins (mkStdGen 22)  
(True,False,True)  
ghci> threeCoins (mkStdGen 943)  
(True,False,True)  
ghci> threeCoins (mkStdGen 944)  
(True,True,True)

randoms

ghci> take 5 $ randoms (mkStdGen 11) :: [Int]  
[-1807975507,545074951,-1015194702,-1622477312,-502893664]  
ghci> take 5 $ randoms (mkStdGen 11) :: [Bool]  
[True,True,True,True,False]  
ghci> take 5 $ randoms (mkStdGen 11) :: [Float]  
[7.904789e-2,0.62691015,0.26363158,0.12223756,0.38291094]

randoms' :: (RandomGen g, Random a) => g -> [a]  
randoms' gen = let (value, newGen) = random gen in value:randoms' newGen

finiteRandoms :: (RandomGen g, Random a, Num n) => n -> g -> ([a], g)  
finiteRandoms 0 gen = ([], gen)  
finiteRandoms n gen =   
    let (value, newGen) = random gen  
        (restOfList, finalGen) = finiteRandoms (n-1) newGen  
    in  (value:restOfList, finalGen)

randomR

ghci> randomR (1,6) (mkStdGen 359353)  
(6,1494289578 40692)  
ghci> randomR (1,6) (mkStdGen 35935335)  
(3,1250031057 40692)

randomRs

ghci> take 10 $ randomRs ('a','z') (mkStdGen 3) :: [Char]  
"ndkxbvmomg"

getStdGen I/O action

import System.Random  
  
main = do  
    gen <- getStdGen  
    putStr $ take 20 (randomRs ('a','z') gen)

Get twice is the same!

import System.Random  
  
main = do  
    gen <- getStdGen  
    putStrLn $ take 20 (randomRs ('a','z') gen)  
    gen2 <- getStdGen  
    putStr $ take 20 (randomRs ('a','z') gen2)

import System.Random  
import Data.List  
  
main = do  
    gen <- getStdGen  
    let randomChars = randomRs ('a','z') gen  
        (first20, rest) = splitAt 20 randomChars  
        (second20, _) = splitAt 20 rest  
    putStrLn first20  
    putStr second20

newStdGen

  • Splits our current random generator into two generators
  • The global one gets updated as well, getStdGen again will get different one
import System.Random  
  
main = do     
    gen <- getStdGen     
    putStrLn $ take 20 (randomRs ('a','z') gen)     
    gen' <- newStdGen  
    putStr $ take 20 (randomRs ('a','z') gen')

Example: guess the number

import System.Random  
import Control.Monad(when)  
  
main = do  
    gen <- getStdGen  
    askForNumber gen  
  
askForNumber :: StdGen -> IO ()  
askForNumber gen = do  
    let (randNumber, newGen) = randomR (1,10) gen :: (Int, StdGen)  
    putStr "Which number in the range from 1 to 10 am I thinking of? "  
    numberString <- getLine  
    when (not $ null numberString) $ do  
        let number = read numberString  
        if randNumber == number   
            then putStrLn "You are correct!"  
            else putStrLn $ "Sorry, it was " ++ show randNumber  
        askForNumber newGen

Example: guess the number

import System.Random  
import Control.Monad(when)  
  
main = do  
    gen <- getStdGen  
    let (randNumber, _) = randomR (1,10) gen :: (Int, StdGen)     
    putStr "Which number in the range from 1 to 10 am I thinking of? "  
    numberString <- getLine  
    when (not $ null numberString) $ do  
        let number = read numberString  
        if randNumber == number  
            then putStrLn "You are correct!"  
            else putStrLn $ "Sorry, it was " ++ show randNumber  
        newStdGen  
        main

{{% /section %}}


{{% section %}}

Bytestrings

  • String is too lazy sometimes
  • Bytestrings are sort of like lists, only each element is one byte (or 8 bits) in size.

Data.ByteString

  • strict version
  • no laziness
  • a series of bytes in an array
  • less overhead because there are no thunks (the technical term for promise) involved
  • memory consumtion

Data.ByteString.Lazy

  • lazy version
  • there are as many thunks in a list as there are elements
  • stored in chunks, each chunk has a size of 64K.
  • fits neatly into CPU's L2 cache
  • signature similar to List but:
    • ByteString instead of [a]
    • Word8 instead of a

pack

pack :: [Word8] -> ByteString

ghci> B.pack [99,97,110]  
Chunk "can" Empty  
ghci> B.pack [98..120]  
Chunk "bcdefghijklmnopqrstuvwx" Empty

unpack

It takes a bytestring and turns it into a list of bytes.


fromChunks/toChunks

fromChunks takes a list of strict bytestrings and converts it to a lazy bytestring. toChunks takes a lazy bytestring and converts it to a list of strict ones.

ghci> B.fromChunks [S.pack [40,41,42], S.pack [43,44,45], S.pack [46,47,48]]  
Chunk "()*" (Chunk "+,-" (Chunk "./0" Empty))

cons/cons'/empty

  • The bytestring version of :
  • Lazy/Strict version
ghci> B.cons 85 $ B.pack [80,81,82,84]  
Chunk "U" (Chunk "PQRT" Empty)  
ghci> B.cons' 85 $ B.pack [80,81,82,84]  
Chunk "UPQRT" Empty  
ghci> foldr B.cons B.empty [50..60]  
Chunk "2" (Chunk "3" (Chunk "4" (Chunk "5" (Chunk "6" (Chunk "7" (Chunk "8" (Chunk "9" (Chunk ":" (Chunk ";" (Chunk "<"  
Empty))))))))))  
ghci> foldr B.cons' B.empty [50..60]  
Chunk "23456789:;<" Empty

analogous to Data.List

head, tail, init, null, length, map, reverse, foldl, foldr, concat, takeWhile, filter


analogous to System.IO/System.Directory

readFile :: FilePath -> IO ByteString

import System.Environment  
import qualified Data.ByteString.Lazy as B  
  
main = do  
    (fileName1:fileName2:_) <- getArgs  
    copyFile fileName1 fileName2  
  
copyFile :: FilePath -> FilePath -> IO ()  
copyFile source dest = do  
    contents <- B.readFile source  
    B.writeFile dest contents

{{% /section %}}


{{% section %}}

Exceptions

  • Take advantange on Maybe/Either in pure code!
  • I/O code (i.e. impure code) can throw exceptions.
  • Pure code can throw exceptions, but it they can only be caught in the I/O
ghci> 4 `div` 0  
*** Exception: divide by zero  
ghci> head []  
*** Exception: Prelude.head: empty list

doesFileExist

doesFileExist :: FilePath -> IO Bool

import System.Environment  
import System.IO  
import System.Directory  
  
main = do (fileName:_) <- getArgs  
          fileExists <- doesFileExist fileName  
          if fileExists  
              then do contents <- readFile fileName  
                      putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!"  
              else do putStrLn "The file doesn't exist!"

catch

catch :: IO a -> (IOError -> IO a) -> IO a

import System.Environment  
import System.IO  
import System.IO.Error  
  
main = toTry `catch` handler  
              
toTry :: IO ()  
toTry = do (fileName:_) <- getArgs  
           contents <- readFile fileName  
           putStrLn $ "The file has " ++ show (length (lines contents)) ++ " lines!"  
  
handler :: IOError -> IO ()  
handler e = putStrLn "Whoops, had some trouble!"

$ runhaskell count_lines.hs i_exist.txt  
The file has 3 lines!  
  
$ runhaskell count_lines.hs i_dont_exist.txt  
Whoops, had some trouble!

ioError/isDoesNotExistError

  • isDoesNotExistError :: IOError -> Bool
  • ioError :: IOException -> IO a
handler :: IOError -> IO ()  
handler e  
    | isDoesNotExistError e = putStrLn "The file doesn't exist!"  
    | otherwise = ioError e

More errors

  • isAlreadyExistsError
  • isDoesNotExistError
  • isAlreadyInUseError
  • isFullError
  • isEOFError
  • isIllegalOperation
  • isPermissionError
  • isUserError: evaluates to True when we use the function userError: ioError $ userError "remote computer unplugged!"
handler :: IOError -> IO ()  
handler e  
    | isDoesNotExistError e = putStrLn "The file doesn't exist!"  
    | isFullError e = freeSomeSpace  
    | isIllegalOperation e = notifyCops  
    | otherwise = ioError e

ioe-* functions

ioeGetFileName :: IOError -> Maybe FilePath

handler :: IOError -> IO ()     
handler e     
    | isDoesNotExistError e =   
        case ioeGetFileName e of Just path -> putStrLn $ "Whoops! File does not exist at: " ++ path  
                                 Nothing -> putStrLn "Whoops! File does not exist at unknown location!"  
    | otherwise = ioError e

multiple catch handlers

main = do toTry `catch` handler1  
          thenTryThis `catch` handler2  
          launchRockets

{{% /section %}}