In chapter 9 of Real World Haskell (O'Sullivan, Goerzen, and Stewart, 2009, page 214) we read:
However, the doesDirectoryExist function is an action; its return type is IO Bool, not Bool.
This is why we have to write things like:
exists <- doesDirectoryExist path
if exists
then ...
else ...
instead of:
if doesDirectoryExist path
then ...
else ...
However, it looks wrong to me to say that doesDirectoryExist is an action. To my mind an action has side effects, but doesDirectoryExist is a pure function and just returns a value with no side-effects. Why then is doesDirectoryExist in the IO monad; why can't we just use the "if doesDirectoryExist path" version above? This seems to go against the command-query separation principle.
The reason is that, while doesDirectoryExist produces no side-effects itself, it does depend upon the side-effects produced by previous IO actions. Consider the following code:
if doesDirectoryExist path
then do
createDirectory path
if doesDirectoryExist path
then ...
else ...
else ...
If doesDirectoryExist was not in the IO monad, the second of the two occurrences of "doesDirectoryExist path" could just return the memoized value of the first occurrence, and we would not be able to detect that the path directory had been created as a side-effect of the createDirectory function.
This reasoning also explains why the other functions returning IO Bool, such as doesFileExist, hIsEOF and hIsWritable, must also be in the IO monad.