我有一个并发帮助器,它比IO
略微包装。对于它,>>=
与vanilla IO
一样是顺序的,但>>
同时执行其参数。
我想将此类型设为MonadThrow
的实例(来自exceptions包)。但是,文档中MonadThrow
必须满足的这项法律让我暂停:
throwM e >> x = throwM e
我的monad并非如此。由于throwM e
和x
将同时执行,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)
答案 0 :(得分:4)
真正帮助我思考Haskell的IO x
的一个心理模型是精神上将其称为“包含x
的程序(即一些与Haskell同构的内部表示x
)”。 Haskell构建程序但不执行它们;您只在实际运行程序时执行它们。 a >> b
等同于a >>= \_ -> b
的monad定义因此>>
是 in-sequence ,全程停止。这就是MonadThrow假设throwM e >> x
与throwM 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;效果。在这种情况下,并发性是有道理的。