记住有效的函数

时间:2015-09-11 14:33:32

标签: haskell memoization unsafe-perform-io

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

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简单地运行自动机。我们可以通过memo完成每个步骤来节省很多(我的意思很多:它确实可以节省数小时)重新计算:

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 -> getLine)不断提取行,尽管传递了相同的参数。

2 个答案:

答案 0 :(得分:2)

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

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可以用于多个不同的功能。如果我们只关心一个memoized有效函数,我们可以改为使用状态转换器。

{-# 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

此变换器增加了从memoized有效函数中查找值的功能

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 - 样式类,用于为特定函数提供memoization的monad,以及一个更精细的构建它们的机制,它们不一定使用Map

答案 1 :(得分:0)

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

你要做的不是记忆,它实际上是控制对内部monad的调用。部分线索是Cellular不是monad,因此CellularT不是monad变压器。

我认为你需要做的是拥有一个纯函数来计算每个细胞所需的效果,然后迭代细胞来对效果进行排序。这将您的细胞自动机械学(您已经拥有并且看起来很好)与您有效的机制分开。目前你似乎试图在计算效果的同时执行效果,这会导致你的问题。

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