有一个懒惰的Session IO Monad吗?

4
你有一系列的操作,由于某些高固定开销(例如数据包头或建立连接),它们更喜欢以块的方式执行。限制在于有时下一个动作取决于上一个动作的结果,在这种情况下,所有挂起的操作将同时执行。
示例:
mySession :: Session IO ()
    a <- readit  -- nothing happens yet
    b <- readit  -- nothing happens yet
    c <- readit  -- nothing happens yet
    if a       -- all three readits execute because we need a
      then write "a"
      else write "..."
    if b || c  -- b and c already available
      ...

这让我想起很多 Haskell 的概念,但我无法准确定义。

当然,你可以做一些明显的事情,比如:

[a,b,c] <- batch([readit, readit, readit])

但出于平滑度的目的,我希望向用户隐藏分块的事实。

不确定“Session”是否是正确的词语。也许你可以建议更好的吗?(我想到了Packet,Batch,Chunk和Deferred)

更新

我认为昨晚我在手机上读到了一个非常好的答案,但今天回来寻找时它已经消失了。我是在做梦吗?


5
你正在描述unsafeInterleaveIO。远离它,它是邪恶的。你会在纯代码内运行IO操作。将a+b改为b+a可能会破坏你的程序,因为IO操作按相反顺序运行。别说我没警告过你。:-P - chi
1
@chi 嗯,这个想法是在任何一个值被强制时强制整个块(a、b 和 c)。这不会改变顺序。 - Yuras
unsafeInterleaveIO并不像unsafePerformIO那样邪恶,但如果使用不当仍可能引入错误。例如:如果你正在使用它将某些内容加载到列表中,你必须考虑迭代列表本身是否会触发IO操作,或者它是否会通过评估实际元素来触发。 - Jeremy List
3个回答

3
我认为你无法完全按照你的意愿执行,因为你描述的方式利用了Haskell的惰性求值使得对a的求值强制计算bc,而且没有办法在未指定的值上使用seq
我能做的是构建一个单子变换器,延迟通过>>顺序排列的操作,以便它们可以一起执行:
data Session m a = Session { pending :: [ m () ], final :: m a }                    

runSession :: Monad m => Session m a -> m a                                         
runSession (Session ms ma) = foldr (flip (>>)) (return ()) ms >> ma                            

instance Monad m => Monad (Session m) where                                         
  return = Session [] . return                                                      
  s >>= f = Session [] $ runSession s >>= (runSession . f)                          
  (Session ms ma) >> (Session ms' ma') =
    Session (ms' ++ (ma >> return ()) : ms) ma'

这违反了一些单子法则,但可以让你做类似这样的事情:
 liftIO :: IO a -> Session IO a  
 liftIO = Session []             

 exampleSession :: Session IO Int
 exampleSession = do             
   liftIO $ putStrLn "one"       
   liftIO $ putStrLn "two"       
   liftIO $ putStrLn "three"     
   liftIO $ putStrLn "four"      
   trace "five" $ return 5       

并获得

ghci> runSession exampleSession
five
one
two
three
four
5
ghci> length (pending exampleSession)
4

这很有趣。为什么会违反某些单子定律呢?它似乎会按照 IO 的顺序执行。好的,我尝试在“two”之后的序列中插入 l <- liftIO getLine。现在,“five”在获取行之后打印,而不是第一件事。这似乎令人不安。 - Michael Fox
你可以通过将 (>>=) 的定义与 (>>) 相似来避免违反单子定律。然后可以排除 (>>) 的定义。 - is7s
is7s:对我来说,(>>=)的定义并不明显。 - rampion

1

3
很遗憾,在我看来,这里的问题与Haxl不同。Haxl是可应用的(所以连续的步骤不依赖于先前步骤的结果),而这里存在数据依赖关系。 - Yuras
@Yuras 看起来有一个Haxl Monad。 这是我正在寻找的Monad吗?很难说。 我还在仔细阅读haddock文档。 - Michael Fox
@MichaelFox 我现在不确定,因为我可能误解了问题。我会尝试稍后提出更有力的主张。 - Yuras

0
你可以使用unsafeInterleaveIO函数。如果不小心使用,它可能会给你的程序引入bug,因此它是一种危险的函数。但是它可以实现你所需要的功能。
你可以像这样将其插入示例代码中:
lazyReadits :: IO [a]
lazyReadits = unsafeInterleaveIO $ do
  a <- readit
  r <- lazyReadits
  return (a:r)

unsafeInterleaveIO 使整个操作变为惰性的,但一旦开始评估,它将评估为严格的。这意味着在我的上面的例子中:readit 将在某些东西测试返回的列表是否为空时立即运行。如果我使用 mapM unsafeInterleaveIO (replicate 3 readit),那么只有当实际列表元素被评估时才会运行 readit,这将使列表内容取决于检查其元素的顺序,这是 unsafeInterleaveIO 可能引入错误的一个例子。


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