I'm currently playing around with Polysemy, rewriting a small toy project of mine to get used to it. I'm stumbling upon a piece of code that uses pooledMapConcurrentlyN, so basically a parallel version of traverse with bounded concurrency.
I can strip my example down to this:
foo :: Sem r Int
foo = do
res <- pooledMapConcurrentlyN 3 action (["foo", "bar", "baz"] :: [String])
pure $ sum res
action :: String -> Sem r Int
action = pure. length
This doesn't compile because there's no instance for MonadUnliftIO (Sem r). It does compile when I use traverse, but I'm looking for a concurrent version. I'm not sure which way I should go now.
I see the following options:
- Implement a
MonadUnliftIO (Sem r)instance. I see that there were some discussions about adding/implementing such an instance in this GitHub issue. However, it's not clear to me whether it's a good idea to do so. - Using something other than
pooledMapConcurrentlyNthat gives me an equivalent behavior. I know that there'sparTraversefrom the par-dual package, but that would require aParDualinstance. Theparallelpackage could make a solution possible as well, but I'm not familiar with that so I can't tell if it's possible. - Model the parallel traverse as an effect. I tried it, but I couldn't manage to get an implementation for the effect. The effect definition I tried looks like this:
data ParTraverse m a where
TraverseP :: (Traversable t) => Int -> (a -> m b) -> t a -> ParTraverse m (t b)
I'm not really familiar yet with neither GADTs nor Polysemy, so it's possible that I'm missing something obvious here.
EDIT: As pointed out in the answer below, the most appropriate solution is to model this as an effect and handle the concurrency in the effect interpretation as opposed to the business logic. This means that I'm looking for a higher order effect (?) similar to the ParTraverse effect above:
data ParTraverse m a where
TraverseP :: (Traversable t) => (a -> m b) -> t a -> ParTraverse m (t b)
makeSem ''ParTraverse
parTraverseToIO :: (Member (Embed IO) r) => Sem (ParTraverse ': r) a -> Sem r a
parTraverseToIO = interpretH $ \case
TraverseP f ta -> do
_something
I'm not sure whether this type signature is correct or not (should the action have type a -> Sem r b? The signature for traverse has an Applicative constraint on m, how would I model that?)
As for the
ParTraverseimplementation, this is what I replied over on github, for a version specialized to[]fort:Some explanations for the combinators used inside
interpretH, where we operate in theTacticalenvironment:a -> m b, wheremis instantiated toSem rInitialinside the interpreter, we have to usebindTto get a function that is something likef a -> Sem r (f b), withfbeing the monadic state of the interpreters.pooledMapConcurrentlyon theSem rInitialdirectly, becauseMember (Final IO)is only given forr.tacontains the input forf, but since we lifted that to expectf a, we also have to callpureTon each element ofta, usingtraversesince it is a monadic action.bindT(andrunT) produceSems that still have the current effect,ParTraverse, at the head, because the effect has to be interpreted within the wrappedSem(passed in asa -> m b). This even allows to use a different interpreter for the inner program. In our case, we simply runparTraverseToIOon the result offagain. After that, we have to lift thisSemback into theTacticalenvironment (which is just another effect at the head), so we useraise.fproducesf (Maybe b)as result, we need to unpack this in order to get the return type right. For that, we can use the inspector, which transformsftoMaybe, giving usMaybe (Maybe b), which we can then flatten into a list.For completeness, here's the implementation of
pooledMapConcurrently, written by KingoftheHomeless: