Does IO monad become strict when bang pattern is used?

Viewed 170

I expect the following code snippet:

main = do
    let !x = [2,3,5,2,3,5,6,7,1,3,0,1]
    begin <- getCPUTime
    let !rx = reverse x
    end <- getCPUTime
    putStrLn $ "Calculation time: " ++ show (end - begin) ++ " ps."
    putStrLn $ "Result: " ++ show rx

is identical to the following version:

main = do
    let x = [2,3,5,2,3,5,6,7,1,3,0,1]
    begin <- x `seq` getCPUTime
    let rx = reverse x
    end <- rx `seq` getCPUTime
    putStrLn $ "Calculation time: " ++ show (end - begin) ++ " ps."
    putStrLn $ "Result: " ++ show rx

Is this true? This is false if x and rx evaluates to WHNF when "needed", in the first version.

Btw, I want to propose a syntactic sugar for deep evaluation, named "double bang pattern".

1 Answers

The specific code samples you've given generate identical compiled code. If you take the following program:

{-# LANGUAGE BangPatterns #-}

module Bang where

import System.CPUTime

main1 = do
    let !x = [2,3,5,2,3,5,6,7,1,3,0,1]
    begin <- getCPUTime
    let !rx = reverse x
    end <- getCPUTime
    putStrLn $ "Calculation time: " ++ show (end - begin) ++ " ps."
    putStrLn $ "Result: " ++ show rx

main2 = do
    let x = [2,3,5,2,3,5,6,7,1,3,0,1]
    begin <- x `seq` getCPUTime
    let rx = reverse x
    end <- rx `seq` getCPUTime
    putStrLn $ "Calculation time: " ++ show (end - begin) ++ " ps."
    putStrLn $ "Result: " ++ show rx

and compile it with (with GHC version 8.6.5):

stack ghc -- -dsuppress-all -dsuppress-uniques -ddump-simpl -fforce-recomp -O2 Bang.hs

you will find in the dumped GHC core that main1 and main2 are compiled to exactly the same code, which is actually pulled out into a separate main4 function:

main1
main1 = main4 `cast` <Co:3>

main2
main2 = main4 `cast` <Co:3>

HOWEVER, in general, the let !x = ... construct isn't precisely equivalent to using let x = ... followed by x `seq` y. For example, the following two IO actions are different:

foo :: IO ()
foo = do
  let !x = undefined
  return ()

bar :: IO ()
bar = do
  let x = undefined
  return $ x `seq` ()

The first generates an exception immediately:

main = do
    print 1
    foo       -- EXCEPTION!
    print 2

The second does nothing when executed but will generate an exception if you try to scrutinize it's result:

main = do
    print 1
    bar        -- does nothing
    print 2
    x <- bar   -- also does nothing
    print 3
    () <- bar  -- EXCEPTION!
    print 4

I believe that after desugaring, bar and foo are equivalent to:

bar = return (undefined `seq` ())
foo = undefined `seq` return ()

which explains their different behavior.

Related