2

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!

Andrey Kachow
  • 936
  • 7
  • 22
  • 1
    "*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?*" Possible yes, with `unsafePerformIO`, but this is *not* a good idea. You should just construct a function that handles the I/O and write "pure" functions that do the processing. – Willem Van Onsem Jul 25 '21 at 20:32
  • 3
    If this is just for quick-and-dirty debugging, you might want to look at [Debug.Trace](https://hackage.haskell.org/package/base-4.15.0.0/docs/Debug-Trace.html). However, as the disclaimer at the start of the documentation says, this must not be used in production. This type of thing is unfortunately something Haskell doesn't do so well on - Haskell enforces a strict separation between I/O code and pure code, which works wonderfully well most of the time but sadly does mean it's not easy to just stick a "print statement" in for debugging as you can do in most languages. – Robin Zigmond Jul 25 '21 at 20:35
  • 3
    The whole point of having an IO monad is that functions that do IO _must_ have a result type involving IO. Unlike other languages, Haskell forces the programmer to advertise "this code has side effect" in the type. There are some exceptions which are allowed for debugging purposes (`Debug.Trace`) or to go lower-level (in a potentially unsafe way). I strongly recommend to avoid unsafe functions -- they are called unsafe for a reason; if you do use them, expect weird unexpected behavior. Regular code simply uses `myFunc :: A -> B -> IO C` instead, exposing IO in the type. – chi Jul 25 '21 at 22:28
  • @chi, there are a few other patterns that are *fairly* safe. One (which was introduced to me by an Okasaki etc al paper) involves unsafe actions updating the contents of a reference with a better representation of the same abstract value. As they say, this is basically a generalization of lazy evaluation. – dfeuer Jul 25 '21 at 23:18
  • 1
    @dfeuer Nothing that a Haskell beginner should be aware of, IMO. There are legitimate, safe uses of unsafe primitives, but those 1) are mostly ways to optimize something that could be done in another way, 2) require a high level of expertise to guarantee the semantics won't be broken. – chi Jul 25 '21 at 23:44
  • @chi, oh, I agree. Beginners should steer well clear, and others usually should as well. – dfeuer Jul 26 '21 at 00:08

2 Answers2

5

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.

chi
  • 111,837
  • 3
  • 133
  • 218
1

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.

talex
  • 17,973
  • 3
  • 29
  • 66