Is my concurrency monad a valid MonadThrow instance?
I have a concurrency helper that is a little wrapper around IO
. For him, it >>=
is consistent, like with vanilla IO
, but >>
fulfills its arguments at the same time.
I want to make this type an instance MonadThrow
(from the exceptions package ). However, this law, which is consistent with the documentation MonadThrow
, satisfies, gives me a pause:
throwM e >> x = throwM e
This is not entirely the case with my monad. Since throwM e
both x
will be executed concurrently, it x
may have effects in the outside world or even throw an exception from its own before throwM e
aborting the computation.
Can the law be interpreted in a "weak" way, or should I refrain from writing an instance MonadThrow
?
Edit . Here's a simplified code for my 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)
source to share
I thought about it for a bit. I don't think an instance is MonadThrow
inferior to any other typeclass instance you can define based on that instance Monad
. For example, what should the following code do?
liftIO $ putStrLn "Hello" >> error "foo"
liftIO $ putStrLn "World" >> error "bar"
I think most people believe that the result would be to print "Hello" and then discard UserError "foo"
. However, with your implementation >>
, you have a 50/50 shot whether or not this will happen (well, probably not so evenly split since the first thread will still be forked, but you get the idea).
So, I would say, if you agree that an instance is Monad
not scary, you can add an instance as well MonadThrow
. I'm just not sure if the instance itself Monad
makes sense.
On a related note, it reminds me of Simon Marlowe's haxl conversation. They do something like this, but instead of giving parallel behavior >>
, they pass it to the instance Applicative
. It might be worth considering that for your case, as at least in the prior art.
source to share
One mental model that really helps me think of Haskell IO x
is to mentally express it as "a program that contains x
(ie, some internal representation isomorphic to Haskell x
)". Haskell builds programs, but doesn't execute them; you only execute them when the program starts. The definition of a monad, which is a >> b
equivalent a >>= \_ -> b
, therefore says that it >>
is in a complete stop sequence. This is why MonadThrow assumes it is the throwM e >> x
same as throwM e
- they "are the same program" because they are ordered together, but throwM
exits the first time every time. Thus, you are about to do something contradictory for a lot of people.
It is probably easier to just define your own operator as a parallelism primitive. We really need an operator with a different signature:
(>|<) :: IO a -> IO b -> IO (a, b)
This doesn't shrink a
, but waits for it to complete, so you can, say, run two queries against the database and then wait for both of them to return.
This suggests that you want some kind of instance Applicative
for IO
(or if for applicative superclasses IO you need one newtype PIO x = PIO {runPIO :: IO x}
with the required applicative instance).
The only reason to override >>
is if you want to write something like:
do
a <- beforeEverything
thread1 a
thread2 a
thread3 a
-- no afterEverything possible
but perhaps with the correct applicative we can say instead:
do
a <- beforeEverything
runParallel $ afterEverything <$> thread1 a <*> thread2 a <*> thread3 a
with a little trickery close to what the operator does >>=
(turn f x
in x (operator) f
), we can put afterEverything
after its arguments and get a logical ordering that will keep us sane. The only price we'll pay is more grooves.
source to share
I really don't like this law, it seems like a pretty bad shorthand way of describing the required behavior.
If you require all monadic actions raised to ConcIO
be idempotent and discontinuous, that should be fine. However, this limitation can be overly burdensome, which means you cannot use it ConcIO
as directed.
Why not just use a regular one IO
and define a small operator that calls concurrently
? This will give you more control, and it will also allow you to avoid concurrent calls when needed.
source to share
As other answers to my question have explained, the real problem is that >>
it shouldn't be parallel for a monad.
An additional reason for this, found after a bit of tinkering, is that parallel >>
behaves strangely towards monad transformers.
For example, in this code, messages are printed at the same time:
main :: IO ()
main = runConcIO $ do
ConcIO $ sleep 5 >> putStrLn "aaa"
ConcIO $ sleep 5 >> putStrLn "bbb"
ConcIO $ sleep 5 >> putStrLn "ccc"
But if we add a monad transformer layer, all of a sudden the messages will start printing in sequence:
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"
Interestingly, this does not happen with the applicative composition. If we define a parallel instance Applicative
for ConcIO
and compose it with Either
, three messages are still printed at the same time:
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 ()))
The reason seems to be because Applicative Composition applies effects behind a layer. All "concurrency effects" are executed first, and only then "unsuccessful" effects. In this context, concurrency makes sense.
source to share