This article is a must read for anyone interested in getting a good insight into the input/output (IO) functionality of Haskell.
Input/output (IO) can cause side-effects and hence is implemented as a Monad. The IO Monad takes some input, does some computation and returns a value. The IO action is performed inside a main function. Consider a simple Hello world example:
main = putStrLn Hello, World!
Executing the above code in GHCi produces the following output:
$ ghci hello.hs GHCi, version 7.6.3: http://www.haskell.org/ghc/:? for help Loading package ghc-prim ... linking ... done. Loading package integer-gmp ... linking ... done. Loading package base ... linking ... done. [1 of 1] Compiling Main ( foo.hs, interpreted ) Ok, modules loaded: Main. ghci> main Hello, World!
The type signatures of main and putStrLn are:
main :: IO () putStrLn :: String -> IO ()
putStrLn takes a string as input and prints the string to output. It doesnt return anything, and hence the return type is the empty tuple ().
The getLine function performs an IO to return a string.
ghci> :t getLine getLine :: IO String ghci> name <- getLine Foo ghci> name Foo
The <- extracts the result of the IO string action, unwraps it to obtain the string value, and name gets the value. So, the type of name is:
ghci> :t name name :: String
The do syntax is useful to chain IO together. For example:
main = do putStrLn Enter your name: name <- getLine putStrLn ( Hello ++ name)
Executing the code in GHCi gives the following results:
ghci> main Enter your name : Shakthi Hello Shakthi
The putStr function is similar to the putStrLn function, except that it doesnt emit the new line after printing the output string. Its type signature and an example are shown below:
ghci> :t putStr putStr :: String -> IO () ghci> putStr Alpha Alpha ghci>
The putChar function takes a single character as input, and prints the same to the output. For example:
ghc> :t putChar putChar :: Char -> IO () ghci> putChar s s
The getChar function is similar to the getLine function, except that it takes a Char as input. Its type signature and usage are illustrated below:
ghci> :t getChar getChar :: IO Char ghci> a <- getChar d ghci> a d ghci> :t a a :: Char
The print function type signature is as follows:
ghci> :t print print :: Show a => a -> IO ()
It is a parameterised function, which can take an input of any type that is an instance of the Show type class and prints that to the output. Some examples are given below:
ghci> print 1 1 ghci> print c c ghci> print Hello Hello ghci> print True True
The getContents function reads the input until the end-of-?le (EOF) and returns a string. Its type signature is shown below:
ghci> :t getContents getContents :: IO String
An example of code is demonstrated below. It only outputs lines whose length is less than ?ve characters:
main = do putStrLn Enter text: text <- getContents putStr . unlines . filter (\line -> length line < 5) $ lines text
Testing the above example gives the following output:
ghci> main Enter text: a a it it the the four four empty twelve haskell o o
You can break out of this execution by pressing Ctrl-C at the GHCi prompt.
The openFile, hGetContents, hClose functions can be used to obtain a handle for a ?le, to retrieve the ?le contents, and to close the handle respectively. This is similar to ?le handling in C. Their type signatures are shown below:
ghci> :m System.IO ghci> :t openFile openFile :: FilePath -> IOMode -> IO Handle ghci> :t hGetContents hGetContents :: Handle -> IO String ghci> :t hClose hClose :: Handle -> IO ()
The different IO modes are ReadMode, WriteMode, AppendMode and Read-WriteMode. They are de?ned as follows:
-- | See System.IO.openFile data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode deriving (Eq, Ord, Ix, Enum, Read, Show)
An example code is illustrated below:
import System.IO main = do f <- openFile /etc/resolv.conf ReadMode text <- hGetContents f putStr text hClose f
Executing the code in GHCi produces the following output:
ghci> main # Generated by NetworkManager nameserver 192.168 .1 .1
A temporary ?le can be created using the openTempFile function. It takes as input a directory location, and a pattern string for the ?le name. Its type signature is as follows:
ghci> :t openTempFile openTempFile:: FilePath-> String -> IO (FilePath, Handle)
An example is shown below:
import System.IO import System.Directory (removeFile) main = do (f, handle) <- openTempFile /tmp abc putStrLn f removeFile f hClose handle
You must ensure that you remove the ?le after using it. An example is given below:
ghci> main /tmp/abc2731
The operations on opening a ?le to get a handle, getting the contents and closing the handle can be abstracted to a higher level. The readFile and writeFile functions can be used for this purpose. Their type signatures are as follows:
ghci> :t readFile readFile :: FilePath -> IO String ghci> :t writeFile writeFile :: FilePath -> String -> IO ()
The /etc/resolv.conf ?le is read and written to /tmp/resolv.conf in the following example:
main = do text <-readFile /etc/resolv.conf writeFile /tmp/resolv.conf text
You can also append to a file using the appendFile function:
ghci> :t appendFile appendFile :: FilePath-> String -> IO ()
An example is shown below:
main = do appendFile /tmp/log.txt 1 appendFile /tmp/log.txt 2 appendFile /tmp/log.txt 3
The content of /tmp/log.txt is 123.
The actual de?nitions of readFile, writeFile and appendFile are in the System.IO module in the Haskell base package:
readFile :: FilePath -> IO String readFile name = openFile name ReadMode >>= hGetContents writeFile :: FilePath -> String -> IO() writeFile f txt = withFile f WriteMode (\ hdl-> hPutStr hdl txt) appendFile :: FilePath -> String -> IO () appendFile f txt = withFile f AppendMode (\ hdl -> hPutStr hdl txt)
The System.Environment module has useful functions to read command line arguments. The getArgs function returns an array of arguments passed to the program. The getProgName provides the name of the program being executed. Their type signatures are shown below:
ghci> :m System.Environment ghci> :t getArgs getArgs :: IO [String] ghci> :t getProgName getProgName :: IO String
Here is an example:
import System.Environment main = do args <- getArgs program <- getProgName putStrLn ( Program : ++ program) putStrLn The arguments passed are: mapM putStrLn args
Executing the above listed code produces the following output:
$ ghc--make args.hs [1 of 1] Compiling Main ( args.hs, args.o ) Linking args ... $ ./args 1 2 3 4 5 Program : foo The arguments passed are: 1 2 3 4 5
The mapM function is the map function that works for Monad. Its type signature is:
ghci> :t mapM mapM : Monad m => (a -> m b) -> [a] -> m [b]
The System.Directory module has functions to operate on ?les and directories. A few examples are shown below:
ghci> :t createDirectory createDirectory :: FilePath -> IO () ghci> createDirectory /tmp/foo ghci>
If you try to create a directory that already exists, it will return an exception:
ghci> createDirectory /tmp/bar *** Exception : /tmp/bar : createDirectory : already exists (File exists)
You can use the createDirectoryIfMissing function, and pass a Boolean option to indicate whether to create the directory or not. Its type signature is as follows:
ghci> :t createDirectoryIfMissing createDirectoryIfMissing :: Bool -> FilePath -> IO ()
If True is passed and the directory does not exist, the function will create parent directories as well. If the option is False, it will throw up an error:
ghci> createDirectoryIfMissing False /tmp/a/b/c *** Exception : /tmp/a/b/c : createDirectory : does not exist (No such file or directory) ghci> createDirectoryIfMissing True /tmp/a/b/c ghci>
You can remove directories using the removeDirectory or removeDirectoryRecursive functions. Their type signatures are as follows:
ghci> :t removeDirectory removeDirectory :: FilePath -> IO () ghci> :t removeDirectoryRecursive removeDirectoryRecursive :: FilePath -> IO ()
A few examples are shown below:
ghci> createDirectoryIfMissing True /tmp/a/b/c ghci> ghci> removeDirectory /tmp/a *** Exception : /tmp/a : removeDirectory : unsatisified constraints (Directory not empty) ghci> removeDirectoryRecursive /tmp/a ghci>
The existence of a ?le can be tested with the doesFileExist function. You can check if a directory is present using the doesDirectoryExist function. Their type signatures are:
ghci> :t doesFileExist doesFileExist :: FilePath -> IO Bool ghci> :t doesDirectoryExist doesDirectoryExist :: FilePath -> IO Bool
Some examples that use these functions are shown below:
ghci> doesDirectoryExist /abcd False ghci> doesDirectoryExist /tmp True ghci> doesFileExist /etc/resolv.conf True ghci> doesFileExist /etc/unresolv.conf False
To know the current directory from where you are running the command, you can use the getCurrentDirectory function, and to know the contents in a directory you can use the getDirectoryContents function. Their type signatures are:
ghci> :t getCurrentDirectory getCurrentDirectory :: IO FilePath ghci> :t getDirectoryContents getDirectoryContents :: FilePath -> IO [FilePath]
For example:
ghci> getCurrentDirectory /tmp ghci> getDirectoryContents /etc/init.d [livesys,netconsole,.,..,network,README, functions,livesys-late,influxdb]
The copyFile, renameFile and removeFile functions are used to copy, rename and delete ?les. Their type signatures are shown below:
ghci> :t copyFile copyFile :: FilePath -> FilePath -> IO () ghci> :t renameFile renameFile :: FilePath -> FilePath -> IO () ghci> :t removeFile removeFile :: FilePath -> IO ()
Here is a very contrived example:
import System.Directory main = do copyFile /etc/resolv.conf /tmp/resolv.conf renameFile /tmp/resolv.conf /tmp/resolv.conf.orig removeFile /tmp/resolv.conf.orig
To obtain the ?le permissions, use the getPermissions function:
ghci> :t getPermissions getPermissions :: FilePath -> IO Permissions ghci> getPermissions /etc/resolv.conf Permissions {readable = True, writable = False, executable = False, searchable = False}
It is important to separate pure and impure functions in your code and to include the type signatures for readability. An example is shown below:
-- Pure square :: Int-> Int square x = x * x -- Impure main = do putStrLn Enter number to be squared: number <-readLn print (square number)
The readLn function is a parameterised IO action whose type signature is:
:t readLn readLn :: Read a => IO a
Executing the code produces the following output:
ghci> main Enter number to be squared : 5 25