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.