我正在寻找一种 Haskell 设计,以某种方式组成一串单子动作(通常是 IO),以后的动作依赖于以前的动作,但在某些情况下可以在它们完成之前执行。
到目前为止我想出的解决方案是:
type Future m a = m (m a)
读取:一个单子动作,它启动某个过程并返回一个动作,该动作将返回该过程的结果(可能通过等待该过程完成)。
所以在某些链中a >>= b >>= c
b 得到一个返回 a 的结果的动作。如果 b 评估此操作,它会等待 a 完成,否则它将并行运行。这也意味着,如果某个动作不需要前一个动作的结果作为参数,那么根据定义它不依赖于它,因此依赖关系是显式的。
一些示例代码:
date :: Future IO String -- long process to find out the date
date = do
print "attempting to get date" -- will usually start some thread or process to compute the date
return (print "today") -- will wait for this thread or process and return the computed date
main = do
d <- date -- starts recieving the date
print "foo" -- some other process
d >>= print -- waits until the date has been computed and prints it out
输出:
"attempting to get date"
"foo"
"today"
存在一个问题:如果一个动作决定等待前一个动作,它将始终依赖于之前的所有其他动作(在我的情况下)。但是在上面的例子中,如果 c 决定等待 b 但 b 没有决定等待 a,c 可能会在 a 完成之前开始,这不应该发生。
作为解决方案,我编写了另一个组合运算符:
(>=>) :: Monad m => Future m a -> (m a -> Future m b) -> Future m b
a >=> f = do
r1 <- a
r2 <- f r1
return (r1 >> r2)
所以这将结合“等待动作”并且a >=> b >=> c
工作得很好,如果 c 等待 b 这个等待动作也会等待 a。然而,这种方法还有另一个问题(除此之外,您需要记住使用 >=> 而不是 >>=):可能会多次评估等待操作。如果 b 等待 a 并且 c 等待 b,则等待 b 将连接到等待 a,因此等待 a 将被执行两次。
实际问题在于 >=>:f r1
可能会评估 r1 在这种情况下,它不需要在 return 语句中使用 r2 进行排序(因为它已经被执行,因此 a 已经完成)。但它也可能不是,我不知道。
所以我基本上想要的正是这个,但不可能多次运行等待操作。不幸的是,我在功能设计方面不是很有经验。
所以我希望你能以某种方式启发我如何增强或改变我的设计,或者为我指出一种不同的、更灵活的方法。
编辑根据到目前为止的答案,我想进一步澄清我真正想要的内容:
我不想推迟(甚至跳过)动作的执行,我也不需要线程或类似的并行特性。实际上我正在调用外部进程。一个例子是
backup :: Future IO ExitCode
backup = do
pid <- startProcess "backup"
return (waitForProcessAndGetExitCode pid)
当我现在链接类似的操作时backup >=> otherAction
,otherAction 可以在备份运行时运行(总体上节省了很多时间)。但是 otherAction 可能需要完成备份,这种情况下它可以使用它的参数等待备份并检查是否成功。无论哪种方式,都必须执行备份。
我现在正在寻找一个很好的通用解决方案,最好不要与 IO monad 绑定。
更新我找到了一个适合我的解决方案。我在下面的单独答案中对其进行了描述。