Simon D
Simon D

Reputation: 4290

How is the type resolved in this statement

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

Answers (1)

bheklilr
bheklilr

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 LispVals, or be the specific monad you're using of IOThrowsError.

Upvotes: 4

Related Questions