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)

      

+3


source to share


4 answers


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.

+2


source


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.

+4


source


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.

+1


source


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.

+1


source







All Articles