我正在用Haskell编写一个MUD服务器(MUD =多用户地下城:基本上是一个多用户文本冒险/角色扮演游戏)。游戏世界的数据/状态用大约15个不同的IntMap表示。我的单台转换器堆栈看起来如下:ReaderT MudData IO,其中MudData类型是包含IntMaps的记录类型,每个都在自己的TVar中(我使用STM进行并发):
data MudData = MudData { _armorTblTVar :: TVar (IntMap Armor)
, _clothingTblTVar :: TVar (IntMap Clothing)
, _coinsTblTVar :: TVar (IntMap Coins)...and等。(我用的是镜头,因此是下划线。)
有些函数需要某些IntMap,而另一些函数则需要其他函数。因此,让每个IntMap都在自己的TVar中提供粒度。
但是,在我的代码中出现了一种模式。在处理播放器命令的函数中,我需要在STM中读取(有时是稍后写)我的TVar。因此,这些函数最终在它们的where块中定义了一个STM助手。这些STM帮助程序通常有相当多的readTVar操作,因为大多数命令都需要访问少数几个IntMap。此外,一个给定命令的函数可能会调用一些也需要部分或全部IntMap的纯帮助函数。因此,这些纯帮助函数有时会接受大量的参数(有时超过10个)。
因此,我的代码变得“杂乱无章”,包含了大量的readTVar表达式和函数,这些表达式和函数包含大量的参数。以下是我的问题:这是一种代码气味吗?我是不是缺少了一些能使我的代码更加优雅的抽象?是否有更理想的方法来构造我的数据/代码?
谢谢!
发布于 2015-03-07 20:56:38
是的,这显然会使您的代码变得复杂,并将重要的代码与许多样板细节混淆在一起。有超过4个参数的函数是问题的标志。
我想问一个问题:TVar**s?**,,你真的是通过单独的获得任何东西吗?难道不是过早优化吗?在做出将数据结构拆分到多个独立TVar的设计决策之前,我肯定会做一些度量(参见标准)。您可以创建一个示例测试,对预期的并发线程数和数据更新频率进行建模,并检查通过拥有多个TVar和一个IORef而真正获得或丢失了什么。
请记住:
STM事务中存在多个线程争用公共锁,则事务可以在成功完成之前重新启动几次。因此,在某些情况下,拥有多个锁实际上会使事情变得更糟。IORef。它的原子操作非常快,这可以补偿有一个中央锁。STM或IORef事务。原因是懒惰:您只需要在这样的事务中创建块,而不是评估它们。对于单个原子IORef尤其如此。在这样的事务之外(通过一个检查它们的线程,或者如果您需要更多的控制,您可以决定强制它们;在您的情况下,这是需要的,就好像您的系统在没有任何人观察的情况下进化一样,您可以很容易地积累未评估的块)。如果发现拥有多个TVar确实很关键,那么我可能会用一个自定义monad (在编写我的答案时由@Cirdec描述)编写所有的代码,其实现将被隐藏在主代码中,它将提供读取(或者可能也写)状态部分的功能。然后,它将作为单个STM事务运行,只读取和写入所需的内容,您可以有一个纯版本的monad进行测试。
发布于 2015-03-07 19:56:40
这个问题的解决方案是更改纯辅助函数。我们并不是真的希望它们是纯的,我们想泄漏出一个单一的副作用--不管它们是否读取特定的数据。
假设我们有一个纯粹的功能,它只使用衣服和硬币:
moreVanityThanWealth :: IntMap Clothing -> IntMap Coins -> Bool
moreVanityThanWealth clothing coins = ...通常很高兴知道一个函数只关心衣服和硬币,但是在你的例子中,这个知识是不相关的,只是让人头疼。我们要故意忘记这个细节。如果我们遵循mb14的建议,我们将向助手函数传递一个完整的纯MudData',如下所示。
data MudData' = MudData' { _armorTbl :: IntMap Armor
, _clothingTbl :: IntMap Clothing
, _coinsTbl :: IntMap Coins
moreVanityThanWealth :: MudData' -> Bool
moreVanityThanWealth md =
let clothing = _clothingTbl md
coins = _coinsTbl md
in ...MudData和MudData'几乎完全相同。它们中的一个用TVar包装它的字段,而另一个没有。我们可以修改MudData,这样它就可以使用一个额外的类型参数(类似于* -> *)来包装字段。MudData将有一种稍微与众不同的(* -> *) -> *,它与镜片密切相关,但没有太多的库支持。我称这种模式为模型。
data MudData f = MudData { _armorTbl :: f (IntMap Armor)
, _clothingTbl :: f (IntMap Clothing)
, _coinsTbl :: f (IntMap Coins)我们可以用MudData恢复原始的MudData TVar。我们可以通过在Identity,newtype Identity a = Identity {runIdentity :: a}中包装字段来重新创建纯版本。就MudData Identity而言,我们的函数将编写为
moreVanityThanWealth :: MudData Identity -> Bool
moreVanityThanWealth md =
let clothing = runIdentity . _clothingTbl $ md
coins = runIdentity . _coinsTbl $ md
in ...我们已经成功地忘记了我们使用过的MudData的哪些部分,但是现在我们没有我们想要的锁粒度。我们需要恢复,作为一个副作用,这正是我们刚刚忘记的。如果我们编写了帮助器的STM版本,它将类似于
moreVanityThanWealth :: MudData TVar -> STM Bool
moreVanityThanWealth md =
do
clothing <- readTVar . _clothingTbl $ md
coins <- readTVar . _coinsTbl $ md
return ...这个用于STM的MudData TVar版本与我们刚刚为MudData Identity编写的纯版本几乎完全相同。它们仅根据引用的类型(TVar与Identity)、用于从引用中获取值的函数(readTVar与runIdentity)以及结果如何返回(以STM或作为普通值)而有所不同。如果可以使用相同的函数来同时提供这两种功能,那就太好了。我们将提取这两个函数之间的共同点。为此,我们将引入一个类型类MonadReadRef r m,用于我们可以从其中读取某种类型引用的Monad。r是引用的类型,readRef是从引用中获取值的函数,m是返回结果的方式。下面的MonadReadRef与来自参考文献的MonadRef类密切相关。
{-# LANGUAGE FunctionalDependencies #-}
class Monad m => MonadReadRef r m | m -> r where
readRef :: r a -> m a只要代码在所有MonadReadRef r m上都是参数化的,它就是纯的。我们可以通过使用以下MonadReadRef实例来运行Identity中保存的普通值,从而看到这一点。id in readRef = id与return . runIdentity相同。
instance MonadReadRef Identity Identity where
readRef = id我们将以moreVanityThanWealth的形式重写MonadReadRef。
moreVanityThanWealth :: MonadReadRef r m => MudData r -> m Bool
moreVanityThanWealth md =
do
clothing <- readRef . _clothingTbl $ md
coins <- readRef . _coinsTbl $ md
return ...当我们在MonadReadRef中为TVar添加一个STM实例时,我们可以在STM中使用这些“纯”计算,但是泄漏了读取TVars的副作用。
instance MonadReadRef TVar STM where
readRef = readTVarhttps://stackoverflow.com/questions/28918151
复制相似问题