我开始研究一个将元胞自动机定义为局部过渡函数的项目:
newtype Cellular g a = Cellular { delta :: (g -> a) -> a }
每当g
为Monoid
时,就可以通过在应用本地转换之前移动焦点来定义全局转换。这为我们提供了以下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
在这里失败了,因为它保存了有效的计算而非结果。
我不希望在不使用不安全功能的情况下实现这一目标。我尝试使用unsafePerformIO
,IORef
和Map 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)
不断提取行,尽管传递了相同的参数。
答案 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,了解有关如何执行此操作的一些想法。