How it works...

We have been using IO and functions related to IO in a limited way in our previous recipes. The usage was limited to printing the output using either putStrLn or print. We should spend some time to understand what IO is and how it is inevitable for a Haskell program. 

Haskell works with pure functions without side effects. It means that the evaluation of pure functions does not affect the outside world in any way. To be able to interact with the outside world, the outside world would need to contain memory, a console, file I/O, networking, and so on. The IO monad enables a Haskell program to interact with the outside world. IO monad is the gateway for pure Haskell functions to the outside world. 

By interacting with IO, Haskell functions enforce side effects such as printing to a standard output, opening a file, or doing network operations. This is effectively shown here:

Since the IO operations are imperative, the first step is executed before the following ones. For example, we cannot write to a file without opening it first. It is, hence, logical that IO is an instance of a monad and implicitly Applicative and Functor.

Moving to the recipe, to open the file and print the lines along with the line numbers, the following points are to be noted. The function (getLinesSeq :: Handle -> IO [String]) takes a handle to the file and produces a list of strings in IO monad. We will first check whether we have reached the end of the file using the hIsEOF function. If we have reached the end of the file, we will just return []. Otherwise, we will use Applicative in a very interesting way:

    (:) <$> hGetLine h <*> getLinesSeq h 

This is a very interesting pattern, and Applicative fits perfectly in this. We used hGetLine :: Handle -> IO String to get a single line from the file. The rest of the lines can be retrieved using the getLinesSeq function recursively. Now, we need to put the single line ahead of the rest of the lines returned by getLinesSeq. For the pure list, we can achieve it by the (:) function. Using the Functor <$> and Applicative <*> functions, we can easily represent this pattern. If we use the monadic do notation, this can be written as follows:

    do
line <- hGetLine h
lines <- getLinesSeq h
return (line : lines)

Using the Functor and Applicative patterns, the preceding code can be represented in a very succinct and expressive way.

The function withLineNumbers takes in a monad (any) that represents a list of strings. It again lifts the function zip :: [a] -> [b] -> [(a,b)] to the monad to add the line number to each input line. It again uses the Functor/Applicative pattern:

    -- In fact Monad m is not necessary here.. Applicative m should 
suffice.
withLineNumbers :: Monad m => m [String] -> m [(Int,String)]
withLineNumbers m = zip <$> pure [1..] <*> m

The preceding code can also be written in a monadic notation as follows:

    withLineNumbers m = do
let linenumbers = [1..] -- infinite list [1,2,...]
lines <- m -- Input monad represents a list of lines
return (zip linenumbers lines)

In the main function, we used getArgs to get the arguments to the function. The when function checks if the number of arguments is correct. When it is not correct, we raise an error Provide file name. If we run the program without an argument (or more than one argument), then the error causes an exception to be raised, and the program will terminate. You should see the following output:

We use the withFile :: FilePath -> IOMode -> (Handle -> IO a) -> IO a function. This function takes three arguments:

We use an anonymous function (\h -> ...) to work with the opened file. We call withLineNumbers along with getLinesSeq to get a list of lines with line numbers. We then use Control.Monad.forM_ to print each line using printLine.