如何在 Haskell 中为 IO 专门化 mapM

How to specialize mapM for IO in Haskell

假设我有一个任务代表从 kv 的一些计算,其中一些输入必须从外部获取。

newtype Task k v = Task { run ∷ ∀ f. Monad f ⇒ (k → f v) → f v }

对于某些任务,将使用 mapM,例如获取多个密钥。我想专门针对某些 monads mapM。专门针对 IO monad 我想使用 Control.Concurrent.Async.mapConcurrently 并发执行 IO 操作。

我的第一直觉是引入包装器类型

newtype AsyncIO a = AsyncIO { io :: IO a }

接着介绍

instance Monad AsyncIO

但是这不起作用,因为在当前的 GHC 实现中 mapM 是根据 traverse 定义的,它在 Traversable.

有没有优雅的解决方案?

让 运行 将 mapMmapConcurrently 作为附加参数,或者可能较少作为隐式参数传递,但我不确定后者是否会与f的量化.

嗯,traverse只需要一个Applicative。您可以通过对 IO 使用替代 Applicative 来使 mapM 并行执行其操作(这就是 mapConcurrently 的实现方式)。然而,这个Applicative没有合法的Monad实例:(>>=)和其他Monad操作将与(<*>)和其他[=16]不一致=] 操作。例如。 mf >>= \f -> mx >>= \x -> return (f x) 不会等同于 mf <*> mx,因为 (>>=) 不能并行执行其参数,但 (<*>) 会。 (您可能可以使用 unsafeInterleaveIO 创建一个有效的 Monad 实例,但是,unsafeInterleaveIO。)

您可以做的一件事是传递 Tasks 一个 Applicative 仿函数,与 Monad 分开,然后提供自然转换以将每个计算注入前者进入后者。查找函数也应该在 Applicative 上下文中。

newtype Task k v = Task { run ∷ ∀f f'. (Monad f, Applicative f')
                              ⇒ (∀a. f' a → f a)
                              → (k → f' v) → f v
                        }

如果您没有任何特殊的 Applicative 涉及,只需使用 id 作为自然变换:

runSimple ∷ Monad f ⇒ Task k v → (k → f v) → f v
runSimple t = run t id

对于 IO,特殊的 Applicative 仿函数已经在 Control.Concurrent.Async.Concurrently:

中为您精心打包
runWithParallelIO ∷ Task k v → (k → IO v) → IO v
runWithParallelIO t lookup = run t runConcurrently (Concurrently . lookup)

你会这样写 Task

task ∷ Task _k _v
task = Task go
  where go exec lookup = do _
                            xs <- exec $ mapM lookup _
                            _

如果您发现自己编写的 Task 根本无法从单独的 MonadApplicative 上下文中获益,则可以使用此智能构造函数

taskSimple ∷ (∀f. Monad f ⇒ (k → f v) → f v) → Task k v
taskSimple r = Task (\exec lookup -> r (exec . lookup))

以避免在 exec 中包装每个 lookup。 AFAICT,runSimple . taskSimple = idtaskSimple . runSimple 是幂等的。