我的并发monad是MonadThrow的有效实例吗?

时间:2014-10-23 05:54:21

标签: exception haskell concurrency monads

我有一个并发帮助器,它比IO略微包装。对于它,>>=与vanilla 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)

4 个答案:

答案 0 :(得分:4)

真正帮助我思考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,而是等待它完成,这样你就可以发出两个数据库请求,然后等到它们都回来。

这表明你想要的是Applicative的{​​{1}}实例(或者,当使用Applicative超类IO时,你需要IO所需的Applicative实例)。

覆盖newtype PIO x = PIO {runPIO :: IO x}的唯一原因是你想要写下这样的内容:

>>

但也许有正确的适用性我们可以改为:

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

有一点类似于do a <- beforeEverything runParallel $ afterEverything <$> thread1 a <*> thread2 a <*> thread3 a 运算符所做的一些技巧(将>>=转换为f x)我们可以将x (operator) f置于其参数之后并获得逻辑顺序这将使我们保持理智。我们付出的唯一代价是更多缩进。

答案 1 :(得分:2)

我已经考虑过这个了。我认为MonadThrow实例不比基于Monad实例定义的任何其他类型类实例更糟糕。例如,以下代码应该做什么?

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

我认为大多数人会认为这样做的结果是打印“Hello”然后抛出UserError "foo"。然而,随着你>>的实现,你有一个50/50的镜头是否会发生(好吧,可能不是那么均匀划分,因为第一个线程仍然会先分叉,但你明白了)。

所以我要说:如果你已经接受Monad实例并不可怕,你也可以投入MonadThrow实例。我只是不相信Monad实例本身是有道理的。

在相关的说明中,这让我想起了Simon Marlow关于haxl的演讲。他们在那里执行类似的操作,但不是将并发行为提供给>>,而是将其提供给Applicative实例。对于你的情况也许值得考虑,至少就像现有技术一样。

答案 2 :(得分:1)

我真的不喜欢这个法律,它似乎是描述所需行为的一种相当差的简写方式。

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

为什么不使用普通IO并定义一个调用concurrently的小型运算符?这样可以提供更多控制,并且可以在必要时避免并发呼叫。

答案 3 :(得分: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实例,并使用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组合逐层应用效果。首先是所有&#34;并发效应&#34;发生,然后才发生'#34;失败&#34;效果。在这种情况下,并发性是有道理的。