The IO type can be constructed in all kinds of ways. For instance, using `putStrLn` is probably the most common
putStrLn "hello world" :: IO ()
As IO is an instance of Functor we can use that interface, for instance
fmap (const 1) (putStrLn "hello world") :: IO Int
That would be using IO's functorial interface. As an extension to a functorial interface we also have a monadic interface. This affords us a "most boring" way of creating new IO values
-- very different from putStrLn
-- it doesn't actually represent *doing* anything at all
return "hello world" :: IO String
and also, finally, a way at getting at values "inside" of IO and therefore "compose" values of IO
So, the emphasis is that IO is "just" another type. One shouldn't refer to "the IO monad" as anything privileged unless you're actively referring to using (return, >>=) with IO. Generally "the IO monad" explains very little of IO's power as "being a monad" is a very small part of any type's interface.
---
To be clear, here are some other interfaces we can use from IO (besides just other IO value constructors)
* combining IO with exception handlers with
catch :: IO a -> (e -> IO a) -> IO a
* forking new threads with
forkIO :: IO () -> IO ThreadId
* embedding IO into another type with
-- extremely common
liftIO :: MonadIO m => IO a -> m a
* converting an IO action into an asynchronous action with
async :: IO a -> IO (Async a)
* assigning a IO action pointed at by a finalizer to a memory pointer with
newForeignPtr :: FunPtr (Ptr a -> IO ()) -> Ptr a -> IO (ForeignPtr a)
---
To be clear, here are some other interfaces we can use from IO (besides just other IO value constructors)
* combining IO with exception handlers with
* forking new threads with * embedding IO into another type with * converting an IO action into an asynchronous action with * assigning a IO action pointed at by a finalizer to a memory pointer with