Search code examples
haskellmonadsio-monad

How to specialize mapM for IO in Haskell


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?


Solution

  • 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 Tasks 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.