如何使用纯变量制作 Koen Claessen 的并发 monad?

问题描述

已知如何基于 Koen Claessen 的功能珍珠创建基于 ContT 的纯并发 monad:

data Action m where
  Atom :: m (Action m) -> Action m
  Fork :: [Action m] -> Action m
  Stop :: Action m

fork :: applicative m => [ContT (Action m) m a] -> ContT (Action m) m ()
fork processes = ContT $ \next -> Fork <$> sequenceA (next () : [ process $ const $ pure $ Const | ContT process <- processes ])

我将如何实现 IORefMVar共享变量?或者至少有一个异步/等待机制? 如果传递的数据类型是多态的,则加分。

解决方法

我假设“实现像 IORefsMVars 这样的共享变量”的意思是其他,而不仅仅是让底层 monad m 包含 {{ 1}} 并使用 IO/IORef。这很简单,就像这样:

MVar

将可变变量添加到“穷人的并发 monad”纯粹的传统方法是向 newVar :: a -> ContT (Action IO) IO (IORef a) newVar x = ContT $ \ k -> Atom $ do v <- newIORef x pure $ k v 类型添加额外的操作,以创建、读取和写入可变变量。假设我们有一些类型 Action,它标识 Var m a 类型的可变变量,可以在 a 中创建和访问。

m

请注意,类型参数 data Action m where Atom :: m (Action m) -> Action m Fork :: [Action m] -> Action m Stop :: Action m New :: (Var m a -> Action m) -> Action m Read :: Var m a -> (a -> Action m) -> Action m Write :: Var m a -> a -> Action m -> Action m 没有出现在这些新构造函数的结果类型中,因此它是存在量化的,因此变量可能包含任何类型的值。 a 是一个动作,它以新变量作为参数继续另一个动作; New,给定一个变量,使用该变量的值继续下一个动作;和 Read,给定一个变量和一个新值,在继续之前将值写入变量。

Write 一样,这些将使用在 fork 中产生动作的辅助函数构造:

ContT (Action m) m

之后,您只需要决定合适的 newVar :: (Applicative m) => ContT (Action m) m (Var m a) newVar = ContT $ \ k -> pure (New (Atom . k)) readVar :: (Applicative m) => Var m a -> ContT (Action m) m a readVar v = ContT $ \ k -> pure (Read v (Atom . k)) writeVar :: (Applicative m) => Var m a -> a -> ContT (Action m) m () writeVar v x = ContT $ \ k -> pure (Write v x (Atom (k ()))) 表示。一种方法是数据族,当 Var 可用时,它可以相对容易地使用 IORef/MVar,以及其他类似 IO 索引到 {{1 }} 否则。

Int

当然这只是草图;在 monad-par 包中可以找到更加充实的实现,其设计在 A Monad for Deterministic Parallelism 中进行了描述(Marlow、Newton 和 Peyton Jones 2011);它的 IntMap monad 基本上是一个围绕这样的动作类型的延续 monad,它的 data family Var (m :: Type -> Type) (a :: Type) :: Type data instance Var IO a = IOVar { unIOVar :: !(MVar a) } 抽象的实现方式与此类似,还有一些额外的约束,例如额外的严格性以强制执行确定性并允许内部不纯的纯执行代码(一个 Par 秘密包装了一个 IVar)。