如何正确地强制评估 IO monad 中的纯值?

How to properly force evaluation of pure value in IO monad?

我需要在 IO monad 中强制计算纯值。我在写 C 绑定的更高级别接口。在较低的级别上,我说 newFile 函数和 freeFile 函数。 newFile returns 一些 id,不透明对象 我已经在较低级别上定义了。你基本上不能用它做任何事情但是 使用它来释放文件 and 纯粹计算与 那个文件。

所以,我有(简化):

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path -- ‘fid’ stands for “file id”
  let x = runGetter g fid
  freeFile fid
  return x

这是该功能的初始版本。我们需要先计算x freeFile 被调用。 (代码有效,如果我删除 freeFile 一切都很好, 但我想释放资源,你知道的。)

第一次尝试(我们将使用seq来“强制”评估):

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let x = runGetter g fid
  x `seq` freeFile fid
  return x

分段错误。直接进入文档 seq:

The value of seq a b is bottom if a is bottom, and otherwise equal to b. seq is usually introduced to improve performance by avoiding unneeded laziness.

A note on evaluation order: the expression seq a b does not guarantee that a will be evaluated before b. The only guarantee given by seq is that the both a and b will be evaluated before seq returns a value. In particular, this means that b may be evaluated before a. If you need to guarantee a specific order of evaluation, you must use the function pseq from the "parallel" package.

一个很好的说明,的确,我看到人们对秩序有不同的看法 在这种情况下的评估。 pseq 呢?我需要依赖吗 parallel 就是因为 pseq, 嗯……可能还有别的办法。

{-# LANGUAGE BangPatterns #-}

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let !x = runGetter g fid
  freeFile fid
  return x

分段错误。出色地, that answer 对我不起作用 案件。但是它提示evaluate,我们也试试看:

Control.Exception (evaluate)
Control.Monad (void)

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let x = runGetter g fid
  void $ evaluate x
  freeFile fid
  return x

分段错误。也许我们应该使用 evaluate?

返回的值
Control.Exception (evaluate)
Control.Monad (void)

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let x = runGetter g fid
  x' <- evaluate x
  freeFile fid
  return x'

不,坏主意。也许我们可以链接 seq:

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let x = runGetter g fid
  x `seq` freeFile fid `seq` return x

这行得通。但这是正确的做法吗?也许它只是因为 一些不稳定的优化逻辑?我不知道。如果 seq 关联到 在这种情况下,根据该描述 xfreeFilereturn x returns 其值时进行评估。但同样,他们中的哪一个, x 还是 freeFile 先求值?因为我没有遇到段错误,所以它必须 是x,但是这个结果靠谱吗?你知道如何强制评估吗 xfreeFile 之前 正确地?

一个可能的问题是 newFile 正在做一些懒惰的 IO,而 runGetter 是一个足够懒惰的消费者,运行 seq 在其输出上不会强制newFile 的所有 IO 实际发生。这可以通过使用 deepseq 而不是 seq:

来解决
execGetter :: NFData a => FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  let x = runGetter g fid
  x `deepseq` freeFile fid
  return x

这将解决的另一种可能性是 runGetter 声称是纯净的,但实际上不是(并且是一个懒惰的生产者)。但是,如果是这种情况,正确的解决方法不是在这里使用 deepseq,而是从 runGetter 中消除对 unsafePerformIO 的使用,然后使用:

execGetter :: FilePath -> TagGetter a -> IO a
execGetter path g = do
  fid <- newFile path
  x <- runGetter g fid
  freeFile fid
  return x

然后无需进一步摆弄强制即可工作。

Daniel Wagner 的回答对于这个用例很好,但有时只评估列表的脊柱而不评估列表元素也是有益的(有时列表项没有合理的 NFData 实例).您不能为此使用 deepseq,因为它会评估所有内容。但是,您可以将 seq 与此功能结合使用:

evalList :: [a] -> ()
evalList [] = ()
evalList (_:r) = evalList r