【问题标题】:Is my concurrency monad a valid instance of MonadThrow?我的并发单子是 MonadThrow 的有效实例吗?
【发布时间】:2014-12-18 18:21:18
【问题描述】:

我有一个并发助手,它是IO 的一个小包装。对它来说,>>= 与原版的IO 一样是顺序的,但>> 会同时执行它的参数。

我想让这种类型成为MonadThrow 的实例(来自exceptions 包)。但是,文档中说 MonadThrow 必须满足的这条法律让我停顿了一下:

throwM e >> x = throwM e

我的 monad 并非完全如此。由于throwM ex 将同时执行,x 可以在外部世界产生影响,甚至在throwM e 中断计算之前抛出它自己的异常。

能否以“宽松”的方式解释法律,还是我应该避免编写 MonadThrow 实例?

编辑。这是我的 Monad 的简化代码:

import Control.Concurrent.Async(concurrently)

newtype ConcIO a = ConcIO { runConcIO :: IO a }

instance Monad ConcIO where
   return = ConcIO . return
   f >>= k = ConcIO $ runConcIO f >>= runConcIO . k
   f >> k = ConcIO $ fmap snd $ concurrently (runConcIO f) (runConcIO k)

【问题讨论】:

  • 这是否意味着a >> b = a >>= \ _ -> b 不再成立?
  • @augustss 嗯,这是另一个担心。我不确定我的类型是否真的是单子。如果没有异常发生,a >> b 的返回值将等于a >>= \_ -> b 的返回值。但是在我的 monad 中 b 可能会在 a 完成之前产生影响并抛出异常......

标签: exception haskell concurrency monads


【解决方案1】:

正如我的问题的其他答案所解释的那样,真正的问题是 >> 不应该与 monad 并发。

经过一番修改后发现的另一个原因:并发 >> 在 monad 转换器方面表现异常。

例如,在这段代码中,消息是同时打印的:

main :: IO ()
main = runConcIO $ do
    ConcIO $ sleep 5 >> putStrLn "aaa"
    ConcIO $ sleep 5 >> putStrLn "bbb"
    ConcIO $ sleep 5 >> putStrLn "ccc"

但是如果我们添加一个 monad 转换器层,突然消息开始按顺序打印:

 main :: IO ()
 main = void $ runConcIO $ runExceptT $ do
     lift $ ConcIO $ sleep 5 >> putStrLn "aaa"
     lift $ ConcIO $ sleep 5 >> putStrLn "bbb"
     lift $ ConcIO $ sleep 5 >> putStrLn "ccc"

有趣的是,Applicative 组合不会发生这种情况。如果我们为ConcIO 定义一个并发的Applicative 实例,并用Either 组合它,这三个消息仍然会同时打印:

import Data.Functor.Compose

main :: IO ()
main = void $ runConcIO $ getCompose $  
    (Compose . ConcIO $ sleep 5 >> putStrLn "aaa" >> return (Left ())) *> 
    (Compose . ConcIO $ sleep 5 >> putStrLn "bbb" >> return (Right ())) *> 
    (Compose . ConcIO $ sleep 5 >> putStrLn "ccc" >> return (Right ()))

原因似乎是 Applicative 组合逐层应用效果。首先发生所有“并发效应”,然后才发生“失败”效应。在这种情况下,并发是有意义的。

【讨论】:

    【解决方案2】:

    一个真正帮助我思考 Haskell 的 IO x 的心智模型是在心理上将其表述为“一个包含 x 的程序(即与 Haskell x 同构的一些内部表示)”。 Haskell 构建程序,但不执行它们;您仅在实际运行程序时才执行它们。 a >> b 等同于 a >>= \_ -> b 的 monad 定义因此表示 >>in-sequence,句号。这就是为什么 MonadThrow 假设 throwM e >> xthrowM e 相同的原因——它们“是同一个程序”,因为它们是一起排序的,throwM 每次都会终止前者的执行。所以你会为很多人做一些违反直觉的事情。

    将您自己的运算符定义为并行原语可能会更简单。无论如何,我们真的想要一个具有不同签名的运算符:

    (>|<) :: IO a -> IO b -> IO (a, b)
    

    这不会破坏a,而是等待它完成,这样您就可以发出两个数据库请求,然后等待它们都返回。

    这表明您可能想要的是 IO 的一些 Applicative 实例(或者,当 Applicative 超类 IO 时,您需要 newtype PIO x = PIO {runPIO :: IO x} 和所需的 Applicative 实例)。

    覆盖&gt;&gt; 的唯一原因是你想写这样的东西:

    do 
        a <- beforeEverything
        thread1 a
        thread2 a
        thread3 a
        -- no afterEverything possible
    

    但也许使用正确的 Applicative 我们可以改为:

    do
        a <- beforeEverything
        runParallel $ afterEverything <$> thread1 a <*> thread2 a <*> thread3 a
    

    使用类似于&gt;&gt;= 运算符所做的一些技巧(将f x 转换为x (operator) f),我们也许可以将afterEverything 放在它的参数之后,并获得使我们保持理智的逻辑顺序。我们要付出的唯一代价就是更多的压痕。

    【讨论】:

      【解决方案3】:

      我已经考虑了一点。我认为MonadThrow 实例并不比您可以基于Monad 实例定义的任何其他类型类实例差。比如下面的代码应该做什么?

      liftIO $ putStrLn "Hello" >> error "foo"
      liftIO $ putStrLn "World" >> error "bar"
      

      我想大多数人会认为这样做的结果是打印“Hello”然后抛出UserError "foo"。但是,通过您的 &gt;&gt; 实现,您有 50/50 的可能性来判断是否会发生这种情况(好吧,可能不是那么均匀,因为第一个线程仍然会先分叉,但您明白了)。

      所以我想说:如果你已经接受Monad 实例并不可怕,那么你也可以加入MonadThrow 实例。我只是不相信Monad 实例本身有意义。

      在相关的注释中,这让我想起了 Simon Marlow 关于 haxl 的演讲。他们在那里做了类似的事情,但不是将并发行为给&gt;&gt;,而是给Applicative实例。对于您的情况,这可能也值得考虑,因为至少有现有技术。

      【讨论】:

      • 你是对的,真正的问题是 Monad 实例是假的。感谢 Haxl 指针。 Haxl 具有连续的 &gt;&gt; a 但并发的 *&gt; 似乎有点奇怪。 &gt;&gt;*&gt; 对于 Monad 的行为不应该相似吗?
      【解决方案4】:

      我不太喜欢这条定律,它似乎是描述所需实际行为的一种相当糟糕的速记方式。

      如果您要求提升到ConcIO 的所有一元操作都是幂等且可中断的,那应该没问题。但是,该限制可能过于繁重,这意味着您无法将ConcIO 用于预期目的。

      为什么不直接使用普通的IO 并定义一个调用concurrently 的小运算符呢?这将为您提供更多控制权,并让您在必要时避免并发调用。

      【讨论】:

      • 实际行为是什么?只是好奇。
      • @samboosalis:一个很好的问题,不幸的是我不知道我在写这个答案时在想什么。我怀疑这是对 Haskell 当前异常模型的一种非常短促的挫败感,它既难以推理,也没有我想要的强大。例如,似乎没有办法在模型中表达可恢复的异常。我似乎确实记得认为即使对于给定的例外模型,法律也过于严格,但经过一番思考,我不相信这是真的。
      猜你喜欢
      • 1970-01-01
      • 2011-11-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-08
      • 1970-01-01
      • 2023-02-09
      相关资源
      最近更新 更多