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
pooledMapConcurrentlyN
that gives me an equivalent behavior. I know that there'sparTraverse
from the par-dual package, but that would require aParDual
instance. Theparallel
package 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
ParTraverse
implementation, 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 theTactical
environment:a -> m b
, wherem
is instantiated toSem rInitial
inside the interpreter, we have to usebindT
to get a function that is something likef a -> Sem r (f b)
, withf
being the monadic state of the interpreters.pooledMapConcurrently
on theSem rInitial
directly, becauseMember (Final IO)
is only given forr
.ta
contains the input forf
, but since we lifted that to expectf a
, we also have to callpureT
on each element ofta
, usingtraverse
since it is a monadic action.bindT
(andrunT
) produceSem
s 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 runparTraverseToIO
on the result off
again. After that, we have to lift thisSem
back into theTactical
environment (which is just another effect at the head), so we useraise
.f
producesf (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 transformsf
toMaybe
, giving usMaybe (Maybe b)
, which we can then flatten into a list.For completeness, here's the implementation of
pooledMapConcurrently
, written by KingoftheHomeless: