Reputation: 2967
I've the below code, taken from here:
type Blog a = ReaderT SQLiteHandle IO a
data BlogDBException = BlogDBException String deriving (Show, Typeable)
instance Exception BlogDBException
run :: Blog a -> IO a
run m = do
db <- openConnection "myblog.db"
runReaderT m db --runReaderT :: ReaderT r m a -> (r -> m a)
sql :: String -> Blog (Either String[[Row Value]])
sql query = do
db <- ask --ask :: Monad m => ReaderT r m r
liftIO $ do
putStrLn query
execStatement db query
dbQuery :: Blog [Int]
dbQuery = do
r <- sql "select UID from UIDS;"
case r of
Right [rows] -> return [fromIntegral uid | [(_, Int uid)] <- rows]
Left s -> liftIO $ throwIO (BlogDBException s)
_ -> liftIO $ throwIO (BlogDBException "Invalid result")
I'm trying to understand
1) the exact role of readerT
in data Blog a
?
2) exactly what is runReaderT
doing here?
3) how does the ask
function work?
Does anyone have a straightforward explanation? This is my first time working with the Reader
monad.
Upvotes: 2
Views: 395
Reputation: 2983
1) In this example, the purpose of ReaderT
is to make a value of type SQLiteHandle
available to the functions without adding an additional parameter to each function.
2) runReaderT
"unwraps" the ReaderT
: newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a}
. As you can see, the real representation is r -> m a
: a function from the provided item of type r
to the m a
that you thought you were dealing with directly. Thus ReaderT
isn't really avoiding the fact that a new parameter has to be added to your functions; it's just hiding it for you.
3) runReaderT ask == runReaderT $ ReaderT return == return == r -> m r
Thus ask
is providing access to the "environment" r
(the extra parameter) simply by wrapping it in the underlying monad.
Here's a very simple (admittedly too simple to be realistic) example.
type ModeFlag = Int
g :: ModeFlag -> IO ()
g modeFlag = ... -- take some action based on modeFlag
is equivalent to
h :: ReaderT ModeFlag IO ()
h = do
modeFlag <- ask
... -- take some action based on modeFlag
The utility of this technique wasn't immediately obvious to me when I started learning Haskell. However, consider the case where you have many configuration parameters or you may foresee the need to add more configuration parameters soon. Adding new arguments to functions is very inconvenient. Instead, just pack your configuration values into a record and provide it throughout your application via ReaderT
. There's a function called asks
that is like ask
but also takes a function to apply to the r
value. This can be used to extract certain fields from a record.
data Config :: Config { param1 :: Int, param2 :: String, ... other fields }
doStuff :: ReaderT Config IO ()
doStuff = do
i <- asks param1
s <- asks param2
undefined -- do some stuff
There are some more examples of Reader
and ReaderT
in the documentation (http://hackage.haskell.org/package/mtl-2.2.1/docs/Control-Monad-Reader.html at the bottom), including the local
function which is pretty cool but I haven't used much.
Upvotes: 3