{{% 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
---
### 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
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 %}}