Search code examples
haskelliomonadsyesod

In Yesod/Haskell, how do I use data from IO with the variable interpolation functionality?


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

Solution

  • 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 liftIOs in your code, this can help trim down on how many characters you need to type.