1

我正在尝试做这样的事情:

processListIO :: [A] -> IO [B]
processListIO xs = bracket ini fin $ \s -> mapM (upd s) xs
  where
    ini :: IO (Ptr S)
    upd :: Ptr S -> A -> IO B
    fin :: Ptr S -> IO ()

基本上,这是一个迭代列表的计算,将每个元素映射到其他元素,并在过程中使用内部私有状态。具体的ini, upd, 和fin我想到的来自 C 库,但它们保证“表现良好”,因为它们只是分配新状态,执行唯一副作用是修改状态的计算,然后解除分配状态. 我相信,这意味着我可以安全地放在unsafePerformIO前面并获得一个纯函数:

processList :: [A] -> [B]
processList = unsafePerformIO . processListIO

现在我想做同样的事情,但使用conduit(或者,实际上,任何其他流媒体库)。但是,由于计算本质上是无效的,我希望我的管道是纯的:

processStream :: ConduitT A B Identity ()

甚至更好:

processStream :: forall m. Monad m => ConduitT A B m ()

(我怀疑后者可能没有意义,因为看起来这个技巧对简单列表很有效,只是因为元素是纯的。)

理想情况下,我想对用户完全隐藏计算需要一个状态并进行外部调用的事实,并且只是假装它只是类似于 a scanl(或者mapAccum,正如conduit它所称的那样)。

这可能吗?如何使用conduit(或其他流媒体库)执行此操作?

4

1 回答 1

0

但是,由于计算本质上是无效的,我希望我的管道是纯的:

processStream :: ConduitT A B Identity ()

您不应该这样做,因为它破坏了引用透明度。您要构建的管道将消耗一个A并立即产生一个B,然后重复。从该管道中,您可以通过将其限制为第一次迭代来构造函数A -> B,但它不是纯函数,因为再次调用该函数将对有状态 C 库进行新的调用。

您的第一个函数的纯度processList来自这样一个事实,即在该函数的单个应用程序中,您知道您将对 C 库进行的所有调用。如果你试图让它成为一个管道,你就会失去控制。

无论如何,与纯函数与非纯函数不同,纯管道与使所需资源明确ConduitT A B Identity的不纯管道相比没有任何好处。ConduitT A B M

相反,我建议将您的 C 函数包装在一个抽象 monad 中,以防止用户捕获库状态:

-- Safe interface
newtype M a          -- abstract, internally defined as (Ptr S -> IO a)
consume :: A -> M B
runM :: M a -> a     -- safe if the only way to construct (M a) values is using 'consume' and the 'Monad' instance.

然后你可以很容易地consumeConduitT A B M Void( usemapM ) 包裹起来。据我所知,没有充分的理由希望它M成为Identity. 流媒体库旨在以独立于底层资源(MIdentity)的方式使用。

于 2020-10-16T20:43:29.277 回答