[Published in Open Source For You (OSFY) magazine, December 2014 edition.]

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:  :? 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 doesn’t 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

ghci> name

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:
Hello Shakthi

The putStr function is similar to the putStrLn function, except that it doesn’t emit the newline 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'

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

ghci> a

ghci> :t a
a :: Char

The print function type signature is as follows:

ghci> :t print
print :: Show a => a -> IO ()

It is a parameterized function which can take 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

ghci> print 'c'

ghci> print "Hello"

ghci> print True

The getContents function reads the input until the end-of-file (EOF) and returns a String. Its type signature is shown below:

ghci> :t getContents
getContents :: IO String

An example code is demonstrated below. It only outputs lines whose length is less than five 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:








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 file, to retrieve the file contents, and to close the handle respectively. This is similar to file 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 IOModes are ReadMode, WriteMode, AppendMode and ReadWriteMode. It is defined 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

A temporary file can be created using the openTempFile function. It takes as input a directory location, and a pattern String for the filename. 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 to remove the file after using it. An example is given below:

ghci> main

The operations on opening a file 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 file 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 contents of /tmp/log.txt is ‘123’.

The actual definitions 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: 

The mapM function is the map function that works for Monads. 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 files and directories. A few examples are shown below:

ghci> :t createDirectory
createDirectory :: FilePath -> IO ()

ghci> createDirectory "/tmp/foo"

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"

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> removeDirectory "/tmp/a"
*** Exception: /tmp/a: removeDirectory: unsatisified constraints (Directory not empty)

ghci> removeDirectoryRecursive "/tmp/a"

The existence of a file 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 of using these functions are shown below:

ghci> doesDirectoryExist "/abcd"

ghci> doesDirectoryExist "/tmp"

ghci> doesFileExist "/etc/resolv.conf"

ghci> doesFileExist "/etc/unresolv.conf"

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 

ghci> getDirectoryContents "/etc/init.d"

The copyFile, renameFile and removeFile functions are used to copy, rename and delete files. 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 file 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 parameterized 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: