在IO单子中如何正确强制纯值的评估?

8

我需要在IO单子中强制计算纯值。我正在编写一个高级界面来绑定C语言库。在低层,我有newFile函数和freeFile函数。 newFile返回一些 id,这是我在低层定义的不透明对象。你基本上不能对其进行任何操作,只能用它来释放文件并且纯计算与该文件相关的一些内容。

所以,我有以下代码(简化版):

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

这是函数的初始版本。在调用freeFile之前,我们需要计算x。(如果我删除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文档:

seq a b的值如果a为bottom,那么结果也是bottom, 否则等于bseq一般用来提高性能,避免不必要的惰性求值。

关于求值顺序:表达式seq a b不能保证在求值时先求a 再求bseq只能保证在返回结果之前,ab都会被 求值。特别地,这意味着在a之前,b可能已经被求值。 如果需要保证特定的求值顺序,则必须使用“parallel”包中的函数pseq

好的,确实是个好提示。我见过人们声称在这种情况下求值顺序不同。 那么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

分段错误。嗯,那个答案在我的情况下不起作用。但它建议使用 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与左侧相关联,那么根据该描述,xfreeFile都会在return x返回其值时被计算。但是,它们中的哪一个,x还是freeFile会首先被计算呢?由于我没有得到 seg fault,所以必须是x,但这个结果可靠吗?您知道如何强制在properly之前评估x而不是freeFile吗?


2
seq 只有在已经存在 segfaults 的情况下才会引入 segfaults。你确定问题实际上是 segfault,还是你使用这个术语来表示与通常意义不同的东西?可能是 freeFile 导致了 segfault 吗?(我注意到你声称不会 segfault 的两个也没有执行 freeFile -- 尽管对于未经训练的眼睛来说可能看起来是这样!)此外,除非你在某个地方使用了名字糟糕的惰性 IO(例如 readFileunsafeInterleaveIO),否则根本没有必要强制执行 x - Daniel Wagner
1
此外,我怀疑您可能误解了关于 seq 的注释。请仔细注意在 评估 IO 操作(即找出要执行的具体 IO 操作)和 执行 操作(即执行实际的 IO 操作)之间的区别。该注释仅涉及评估; 您可能需要再次仔细阅读它并明确这个区别! - Daniel Wagner
4
我认为 x \seq` freeFile fid `seq` return x 并不能释放文件,seq一个 IO 动作并不会运行它 - 这个表达式的意思是在返回 return x之前计算xfreeFile fid` 的 WHNF - 但是,计算一个 IO 动作并不会 '执行' 它。 - user2407038
@DanielWagner,每种情况都会出现分段错误(除了最后一个)。如果我在没有execGetter中的freeFile的情况下在GHCi中运行它,我可以在REPL中获取数据,然后手动调用freeFile,一切都正常工作。我的想法是,runGetter使用的内部函数不应该是纯函数,因为它们依赖于现有的fid,在同一程序运行期间可能对不同对象相同(在内部是无效指针)。 - Mark Karpov
@DanielWagner, user2407038,说得好。 - Mark Karpov
newFilefreeFile 的代码是什么?它们来自哪里? - MathematicalOrchid
2个回答

9

可能的一个问题是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

这样做后,应该可以直接运行,无需进一步强制操作。


4
思考后,我得出结论:一切可以被“执行顺序”破坏的东西都不应离开“IO”,因此我的低级函数方法是错误的(这是我第一个绑定项目)。我将尝试纠正错误,然后在这里报告,并可能接受您的答案(我确信您描述的第二种情况是实际发生的)。 - Mark Karpov
4
消除unsafePerformIO就解决了这个问题。我必须说,这并不影响高级API。 - Mark Karpov
1
@Mark 这就是 Haskell 的方式。正确的答案通常是自然而然的。 - PyRulez

0

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

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

简而言之,evalList = foldl const () - effectfully

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接