Say I have a task that represents some computation from k
to v
where some inputs have to be fetched externally.
newtype Task k v = Task { run ∷ ∀ f. Monad f ⇒ (k → f v) → f v }
For some tasks mapM
will be used, e.g. to fetch multiple keys. I want to specialize mapM
for some monads. Specifically for the IO
monad I want to use Control.Concurrent.Async.mapConcurrently
to perform IO actions concurrently.
My first instinct is to introduce a wrapper type
newtype AsyncIO a = AsyncIO { io :: IO a }
and then introduce
instance Monad AsyncIO
However this doesn't work because in the current GHC implementation mapM
is defined in term of traverse
, which is in Traversable
.
Is there an elegant solution for this?
Well, traverse
only needs an Applicative
. You can make mapM
do its actions in parallel by using an alternative Applicative
for IO
(this is how mapConcurrently
is implemented). However, this Applicative
does not have a lawful Monad
instance: (>>=)
and the other Monad
operations will not be consistent with (<*>)
and the other Applicative
operations. E.g. mf >>= \f -> mx >>= \x -> return (f x)
will not be equivalent to mf <*> mx
, as (>>=)
cannot execute its arguments in parallel but (<*>)
will. (You probably could create a working Monad
instance by using unsafeInterleaveIO
, but, well, unsafeInterleaveIO
.)
One of the things you can do is pass Task
s an Applicative
functor, separate from the Monad
, and then provide a natural transformation to inject every computation in the former into the latter. The lookup function should also be in the Applicative
context.
newtype Task k v = Task { run ∷ ∀f f'. (Monad f, Applicative f')
⇒ (∀a. f' a → f a)
→ (k → f' v) → f v
}
If you don't have any special Applicative
involved, just use id
as the natural transformation:
runSimple ∷ Monad f ⇒ Task k v → (k → f v) → f v
runSimple t = run t id
And for IO
, the special Applicative
functor is already nicely packaged up for you in Control.Concurrent.Async.Concurrently
:
runWithParallelIO ∷ Task k v → (k → IO v) → IO v
runWithParallelIO t lookup = run t runConcurrently (Concurrently . lookup)
You'd write a Task
like this:
task ∷ Task _k _v
task = Task go
where go exec lookup = do _
xs <- exec $ mapM lookup _
_
If you find yourself writing a Task
that simply does not benefit from having the separate Monad
and Applicative
contexts, you can use this smart constructor
taskSimple ∷ (∀f. Monad f ⇒ (k → f v) → f v) → Task k v
taskSimple r = Task (\exec lookup -> r (exec . lookup))
to avoid wrapping every lookup
in exec
. AFAICT, runSimple . taskSimple = id
, and taskSimple . runSimple
is idempotent.