在 Yesod/Haskell 中,如何使用具有变量插值功能的 IO 数据?

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

如何从 IO monad 中获取值并将其插入到 yesod 小部件中?

比如我想把一个文件的内容插入到hamlet中:

(readFile "test.txt") >>= \x -> toWidget $ [hamlet| <p> text: #{x} |]

或等同于:

contents <- (readFile "test.txt")
toWidget $ [hamlet| <h2> foo #{contents} |]

关于插值如何与 IO 交互,有一些基本的东西我没有掌握,因为这些类型检查都没有:

Couldn't match type ‘IO’ with ‘WidgetT App IO’ …
    Expected type: WidgetT App IO String
      Actual type: IO String

这些错误发生在 getHomeR 路由函数中。

如果我尝试在 GHCi 中使用预定义函数执行类似操作,我会收到不同的错误。在源文件中,我有一个函数:

makeContent body =
    [hamlet|
        <h2> foo
        <div> #{body}
    |]

在 GHCi 中:

(readFile "test.txt") >>= \x -> makeContent x

由于参数不足,我收到一个错误(我认为这是因为一些我还不明白的模板魔术):

<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

使用 monad 转换器时,要将一些 monad m 转换为转换器 t m,您必须使用 lift 函数:

lift :: (MonadTrans t, Monad m) => m a -> t m a

这实际上是MonadTrans类型类的定义方法,所以实现是特定于转换器t

如果您想在转换器内部执行 IO 操作,您必须为 MonadIO 定义一个实例,它具有 liftIO 方法:

liftIO :: (MonadIO m) => IO a -> m a

MonadIO 的实例不一定是转换器,但是 IOliftIO = id 的实例。这两个函数旨在让您 "pull" 对转换器堆栈进行操作,对于堆栈中的每个级别,您将调用 liftliftIO 一次。

对于你的情况,你有堆栈 WidgetT App IO,带有转换器 WidgetT App 和基础 monad IO,所以你只需要调用一次 liftIO拉动 IO 动作成为 WidgetT App IO monad 中的动作。所以你只要做

liftIO (readFile "test.txt") >>= \x -> makeContent x

许多开发人员(包括我自己)发现 liftIO 当您有很多 IO 操作时,打字有点负担,所以看到类似

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

等等。如果您发现自己在代码中使用了很多 liftIO,这可以帮助 trim 减少您需要键入的字符数。