How do I take values from an IO monad and interpolate into a yesod widget?
For example, I want to interpolate the contents of a file into hamlet:
(readFile "test.txt") >>= \x -> toWidget $ [hamlet| <p> text: #{x} |]
or equivalently:
contents <- (readFile "test.txt")
toWidget $ [hamlet| <h2> foo #{contents} |]
There's something basic I'm not grasping about how interpolation interacts with IO since neither of these type checks:
Couldn't match type ‘IO’ with ‘WidgetT App IO’ …
Expected type: WidgetT App IO String
Actual type: IO String
These errors occur in a getHomeR route function.
If I try to do something similar in GHCi with a predefined function, I get a different error. In the source file, I have a function:
makeContent body =
[hamlet|
<h2> foo
<div> #{body}
|]
in GHCi:
(readFile "test.txt") >>= \x -> makeContent x
And I get an error due to insufficient arguments (I think this is because of some template magic I don't understand yet):
<interactive>:139:33:
Couldn't match expected type ‘IO b’
with actual type ‘t0 -> Text.Blaze.Internal.MarkupM ()’
Relevant bindings include it :: IO b (bound at <interactive>:139:1)
Probable cause: ‘makeContent’ is applied to too few arguments
When working with monad transformers, to convert from some monad m
to a transformer t m
, you have to use the lift
function:
lift :: (MonadTrans t, Monad m) => m a -> t m a
This is actually the defining method of the MonadTrans
typeclass, so the implementation is specific to the transformer t
.
If you want to perform IO
actions inside a transformer, you'll have to define an instance for MonadIO
, which has the liftIO
method:
liftIO :: (MonadIO m) => IO a -> m a
an instance of MonadIO
doesn't have to be a transformer, though, IO
is an instance where liftIO = id
. These two functions are designed to let you "pull" actions up a transformer stack, for each level in the stack you would call lift
or liftIO
once.
For your case, you have the stack WidgetT App IO
, with the transformer WidgetT App
and the base monad IO
, so you only need one call to liftIO
to pull an IO
action up to be an action in the WidgetT App IO
monad. So you would just do
liftIO (readFile "test.txt") >>= \x -> makeContent x
A lot of developers (including myself) find liftIO
a bit of a burden to type when you have a lot of IO
actions, so it's not uncommon to see something like
io :: MonadIO io => IO a -> io a
io = liftIO
putStrLnIO :: MonadIO io => String -> io ()
putStrLnIO = io . putStrLn
printIO :: (MonadIO io, Show a) => a -> io ()
printIO = io . print
readFileIO :: MonadIO io => FilePath -> io String
readFileIO = io . readFile
And so on. If you find yourself using a lot of liftIO
s in your code, this can help trim down on how many characters you need to type.