I try to understand the lifting principle by example, and found this:
Then, if I change it's lift (lift (putStrLn "bla-bla")) to putStrLn "bla-bla", the compiler throw error!
I do this base on my understanding: do block are just syntax sugar, and each line's result are pass into next line. If the next line do not use the args that passed from previous line, the type of args won't cause type confliction, I think.
Take bellow as example, while x<- getLine can past compiling
test:: IO ()
test = do
x <- getLine -- discarded and compiler don't care it type
let a = "bla-bla" -- discarded and compiler don't care it type
putStrLn $ "You type are discarded: "
Now back to the calculations function:
type Data = Map.Map Int String
type StateIO = StateT Data IO
type MaybeStateIO a = MaybeT StateIO a
calculations :: MaybeStateIO ()
calculations = do
lift (lift (putStrLn "bla-bla")) -- if I change this to `putStrLn "bla-bla"`, it failed compiling.
lift (modify (Map.insert 3 "3"))
lift (modify (Map.insert 1 "1"))
mb <- lift (get >>= (return . Map.lookup 1))
lift (lift (print mb))
main = runStateT (runMaybeT calculations) Map.empty
I don't understand is that to compiler require lifting on putStrLn "bla-bla".
Isn't it enough when return value of the last line of do block match the function's return value?
In this example, how does the compiler decide the do block's value type? according to function's signature?
Can anyone explains the lift for me? How do it work, when to use, etc.