Reputation: 4290
Reading "Write Yourself a Scheme in 48 hours" and am confused on this page https://en.wikibooks.org/wiki/Write_Yourself_a_Scheme_in_48_Hours/Adding_Variables_and_Assignment by:
getVar :: Env -> String -> IOThrowsError LispVal
getVar envRef var = do env <- liftIO $ readIORef envRef
maybe (throwError $ UnboundVar "Getting an unbound variable" var)
(liftIO . readIORef)
(lookup var env)
I'm not entirely clear how the type is resolved. Here's my reasoning:
envRef
is of type IORef [(String, IORef LispVal)]
, so readIORef envRef
is of type IO [(String, IORef LispVal)]
.
Now LiftIO
is defined at http://hackage.haskell.org/package/transformers-0.4.1.0/docs/Control-Monad-IO-Class.html as being of type liftIO :: IO a -> m a
and is implemented at https://hackage.haskell.org/package/mtl-1.1.0.2/docs/src/Control-Monad-Error.html#ErrorT. So liftIO $ readIORef envRef
returns m [(String, IORef LispVal)]
for some monad m (which doesn't matter because we just use <-
on it straight away so ignore the monad wrapper) [1].
This means env is [(String, IORef LispVal)]
, so lookup var env
is Maybe IORefLispVal
. For the Just branch of the maybe, liftIO . readIORef
will be m LispVal
, again for some monad m. Given the whole function returns an IOThrowsError LispVal
(which is just an ErrorT LispError IO LispVal
i.e. IO Either LispError LispVal
), so m must be an IOThrowsError
[2].
Now if there are different monad transformers in place, I guess that there could be more than one liftIO with a different type signature in scope. Is this actually the case and if so, to what extent is the chain of reasoning above representative of how Haskell actually determines the types? I'm not happy with the reasoning at [1] or [2], so there's a secondary question of how the monad for liftIO is determined. And finally for a do statement how does do know what monad it's in? Is this determined by the first monad after the <-
? Or some other way?
Upvotes: 3
Views: 114
Reputation: 54058
When you have liftIO $ readIORef envRef
, with the type MonadIO m => m [(String, IORef LispVal)]
, the m
certainly matters! It can only be a monad that implements the MonadIO
typeclass, and it can not be just ignored. It has to be the same monad as returned by the maybe ...
statement. In general, when working with do notation, all statements inside of a given block have to have the same monad, so you could do
main :: IO ()
main = do
putStrLn "Testing"
x <- getLine
putStrLn x
But you can't do
main :: IO ()
main = do
putStrLn "Testing"
x <- Just "Doesn't work!"
putStrLn x
Because IO
and Maybe
are not in the same monad! On top of that, main
is specified to have type IO ()
, so you can't have any opportunity to return something other than an IO
action.
If you're unsure about what type this could be generalized too, I would recommend loading it into GHCi and checking it without a type signature specified. I believe that it would end up being something along the lines of
getVar :: (Eq a, MonadIO m, MonadError LispVal m) => IORef [(a, IORef LispVal)] -> a -> m b
because liftIO $ readIORef envRef
and liftIO . readIORef
means that the monad must be an instance of MonadIO
, in order to perform the lookup
, you need your keys to be an instance of Eq
, and the throwError $ UnboundVar "..."
means that it has to be an instance of MonadError LispVal
. There is nothing that concretes the rest of the signature to return LispVal
s, or be the specific monad you're using of IOThrowsError
.
Upvotes: 4