I don’t think it is possible to change liftIOtoM to make m run twice if you bind it with a let like that.
However, I ran:
module Main (main) where
import qualified Control.Monad.Free as Free
import System.IO.Unsafe (unsafePerformIO)
liftIOtoM :: Monad m => IO a -> m a
liftIOtoM m = do
-- The fictitious state is only used to force @unsafePerformIO@
-- to run @m@ every time @liftIOtoM m@ is evaluated.
s <- getStateM
let p = unsafePerformIO $ do
r <- m
pure (s, r)
case p of
(_, r) -> pure r
where
-- We mark this function as NOINLINE to ensure the compiler cannot reason
-- by unfolding that two calls of @getStateM@ yield the same value.
{-# NOINLINE getStateM #-}
getStateM = pure True
liftIOtoM2 :: Monad m => IO a -> m a
liftIOtoM2 io = return $! unsafePerformIO io
main :: IO ()
main = Free.retract $ do
let m = liftIOtoM (putStrLn "a")
liftIOtoM (putStrLn "a")
liftIOtoM (putStrLn "a")
let n = liftIOtoM2 (putStrLn "b")
liftIOtoM2 (putStrLn "b")
liftIOtoM2 (putStrLn "b")
m
m
n
n
to compare both implementations and I got
a
b
b
b
So you might find my lift useful if you don’t put it in a let.