Haskell. How to do IO inside pure Haskell function? How to print intermediate results while function is being executed?

Viewed 284

In many imperative programming languages like Java, C or Python we can easily add a print function which can give us information about the intermediate state of the program.

My goal is to find a way to do something like that in Haskell. I want the function which not only computes value but also prints something. The function below is a simplified version of what I want to do. My actual function is too complicated and incomprehensive without context to show it here.

My idea is to have a "pure" Haskell function that has an auxiliary function inside which has [Int] -> IO () -> Int type signature. An IO parameter is initialized in the where clause as a do block. But unfortunately, the do block is not executed, when I run the function in GHCI. The function is compiled successfuly though

module Tests where

-- Function returns the sum of the list and tries to print some info
-- but no IO actually happens
pureFuncWithIO :: [Int] -> Int
pureFuncWithIO []   = 0
pureFuncWithIO nums = auxIOfunc nums (return ())
  where
    auxIOfunc [] _       = 0
    auxIOfunc (n : ns) _ = n + auxIOfunc ns (sneakyIOaction n)
    sneakyIOaction n
      = do                               -- Not executed
          putStrLn $ "adding " ++ (show n);
          return () 

Output in GHCI test:

*Tests> pureFuncWithIO [1,2,3,4,5]
15

Meanwhile, I expected something like this:

*Tests> pureFuncWithIO [1,2,3,4,5]
adding 1
adding 2
adding 3
adding 4
adding 5
15

Is it possible to come up with a way to have IO inside, keeping the return type of the outer-most function, not an IO a flavor? Thanks!

2 Answers

This type signature

pureFuncWithIO :: [Int] -> Int

is promising to the caller that no side effect (like prints) will be observed. The compiler will reject any attempt to perform IO. Some exceptions exist for debugging (Debug.Trace), but they are not meant to be left in production code. There also are some "forbidden", unsafe low-level functions which should never be used in regular code -- you should pretend these do not exist at all.

If you want to do IO, you need an IO return type.

pureFuncWithIO :: [Int] -> IO Int

Doing so allows to weave side effects with the rest of the code.

pureFuncWithIO []       = return 0
pureFuncWithIO (n : ns) = do
   putStrLn $ "adding " ++ show n
   res <- pureFuncWithIO ns
   return (n + res)

A major point in the design of Haskell is to have a strict separation of functions which can not do IO and those who can. Doing IO in a non-IO context is what the Haskell type system was designed to prevent.

Your sneakyIOaction is not executed because you pass its result as a parameter to auxIOfunc, but never use that parameter, and haskell being lazy bastard it is never execute it.

If you try to use said parameter you find out that you can't. It's type not allow you to do anithing with it except combine with other IO things.

There is a way to do what you want, but it is on dark side. You need unsafePerformIO

unsafePerformIO :: IO a -> a

That stuff basically allow you to execute any IO. Tricky thing you have to consume result, otherwise you may end up with haskell skip it due to its laziness. You may want to look into seq if you really want to use it, but don't actually need result.

Related