【问题标题】:Memoizing an effectful function记忆一个有效的功能
【发布时间】:2015-09-11 14:33:32
【问题描述】:

我开始研究一个将元胞自动机定义为局部转换函数的项目:

newtype Cellular g a = Cellular { delta :: (g -> a) -> a }

只要gMonoid,就可以通过在应用局部过渡之前移动焦点来定义全局过渡。这为我们提供了以下step 函数:

step :: Monoid g => Cellular g a -> (g -> a) -> (g -> a)
step cell init g = delta cell $ init . (g <>)

现在,我们可以使用iterate 简单地运行自动机。通过memoizing 每个步骤,我们可以节省很多(我的意思是很多:它确实节省了几个小时)重新计算:

run :: (Monoid g, Memoizable g) => Cellular g a -> (g -> a) -> [g -> a]
run cell = iterate (memo . step cell)

我的问题是我将Cellular 概括为CelluarT 以便能够在本地规则中使用副作用(例如复制随机邻居):

newtype CellularT m g a = Cellular { delta :: (g -> m a) -> m a }

但是,我只希望效果运行一次,这样如果您多次询问一个单元格的值是多少,答案都是一致的。 memo 在这里失败了,因为它保存了有效的计算,而不是它的结果。

如果不使用不安全的功能,我不认为这是可以实现的。我尝试使用unsafePerformIOIORefMap g a 来存储已经计算的值:

memoM :: (Ord k, Monad m) => (k -> m v) -> (k -> m v)
memoM =
  let ref = unsafePerformIO (newIORef empty) in
  ref `seq` loopM ref

loopM :: (Monad m, Ord k) => IORef (Map k v) -> (k -> m v) -> (k -> m v)
loopM ref f k =
  let m = unsafePerformIO (readIORef ref) in
  case Map.lookup k m of
    Just v  -> return v
    Nothing -> do
      v <- f k
      let upd = unsafePerformIO (writeIORef ref $ insert k v m)
      upd `seq` return v

但它的行为方式不可预知:memoM putStrLn 被正确记忆,而 memoM (\ str -&gt; getLine) 尽管传递了相同的参数,但仍继续获取行。

【问题讨论】:

  • 您使用的是哪个备忘录库? memoize?
  • 您的数据类型等价于Cont and ContTtype Cellular g a = Cont a gtype CellularT m g a = ContT a m g

标签: haskell memoization unsafe-perform-io


【解决方案1】:

如果你给自己一个机会来分配引用来保存地图,这可以安全地实现。

import Control.Monad.IO.Class

memoM :: (Ord k, MonadIO m) => (k -> m v) -> m (k -> m v)
                 |                           |
                 |        opportunity to allocate the map
                 get to IO correctly

我将使用MVar 而不是IORef 来确保大部分并发正确。这是为了正确性,以防它同时使用,而不是为了性能。对于性能,我们可能会比这更好,并使用双重检查锁或具有更精细锁粒度的并发映射。

import Control.Concurrent    
import Control.Monad.IO.Class    
import qualified Data.Map as Map

memoM :: (Ord k, Monad m, MonadIO m) => (k -> m v) -> m (k -> m v)
memoM once = do 
    mapVar <- liftIO $ newMVar Map.empty    
    return (\k -> inMVar mapVar (lookupInsertM once k))

-- like withMVar, but isn't exception safe   
inMVar :: (MonadIO m) => MVar a -> (a -> m (a, b)) -> m b
inMVar mvar step = do
    (a, b) <- liftIO (takeMVar mvar) >>= step
    liftIO $ putMVar mvar a
    return b

lookupInsertM :: (Ord k, Monad m) => (k -> m v) -> k -> Map.Map k v -> m (Map.Map k v, v)
lookupInsertM once k map = 
    case Map.lookup k map of
        Just v -> return (map, v)
        Nothing -> do
            v <- once k
            return (Map.insert k v map, v)

我们并没有真正使用IO,我们只是在传递状态。任何 monad 都应该能够通过对其应用变压器来做到这一点,那么我们为什么要在 IO 中捣乱呢?这是因为我们希望能够分配这些映射,以便memoM 可以用于多个不同的功能。如果我们只关心一个记忆化的有效函数,我们可以只使用状态转换器。

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

import Control.Applicative
import Control.Monad.Trans.Class
import Control.Monad.Trans.State

newtype MemoT k v m a = MemoT {getMemoT :: StateT (k -> m v, Map.Map k v) m a}
    deriving (Functor, Applicative, Monad, MonadIO)

instance MonadTrans (MemoT k v) where
    lift = MemoT . lift

这个转换器增加了从记忆的有效函数中查找值的能力

lookupMemoT :: (Ord k, Monad m) => k -> MemoT k v m v
lookupMemoT k = MemoT . StateT $ \(once, map) -> do
                                                    (map', v) <- lookupInsertM once k map
                                                    return (v, (once, map'))

要运行它并获取底层 monad,我们需要提供我们想要记忆的有效函数。

runMemoT :: (Monad m) => MemoT k v m a -> (k -> m v) -> m a
runMemoT memo once = evalStateT (getMemoT memo) (once, Map.empty)

我们的MemoT 对每个函数都使用Map。某些功能可能会以其他方式记忆。 monad-memo 包有一个 mtl 风格的 monad 类,为特定函数提供记忆,以及一个更复杂的机制来构建它们,不一定使用 Maps。

【讨论】:

【解决方案2】:

首先,停止尝试使用 unsafePerformIO。它得这个名字是有原因的。

你要做的不是记忆,它实际上控制了对内部单子的调用。部分线索是 Cellular 不是单子,因此 CellularT 不是单子转换器。

我认为您需要做的是有一个纯函数来计算每个单元格所需的效果,然后遍历单元格以对效果进行排序。这将您的细胞自动机机制(您已经拥有并且看起来不错)与您的有效机制分开。目前,您似乎正试图在计算效果的同时执行效果,这导致了您的问题。

您的效果可能需要分为输入阶段和输出阶段,或类似的东西。或者,您的效果实际上更像是一个状态机,其中每个单元的每次迭代都会产生一个结果并期望一个新的输入。在这种情况下,请参阅 my question here 了解有关如何执行此操作的一些想法。

【讨论】:

  • CellularCellularT 是著名的 monad 和 monad 转换器 (ContT),如果您颠倒 ag 参数的顺序。仅仅因为某些东西不是单子转换器并不意味着它不应该是有效的;有很多类似(* -&gt; *) 的东西可能位于 monad 之上,但它们本身却不是 monad。
猜你喜欢
  • 1970-01-01
  • 2019-07-05
  • 1970-01-01
  • 1970-01-01
  • 2018-05-02
  • 1970-01-01
  • 2013-08-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多