I’d abstract message over something that is used insid the unsafePerformIO block, e.g.,
message :: IORef Bool -> String
message ref = unsafePerformIO $ readIORef re >>= ...
{-# NOINLINE message #-}
Of course the problem at use sites is then that we will likely float message isSetRef to top-level and we end up where we began, so we’d need the hypothetical noupdate:
main = do
let before = setMessage
let after () = unsetMessage
let thing () = print (noupdate $ \hide -> message (hide isSetRef))