我在Haskell中编写MUD服务器(MUD =多用户地牢:基本上是一个多用户文本冒险/角色扮演游戏)。游戏世界数据/状态以大约15种不同的IntMap
来表示。我的monad变换器堆栈如下所示:ReaderT MudData IO
,其中MudData
类型是包含IntMap
s的记录类型,每个类型都在自己的TVar
中(I' m使用STM进行并发):
data MudData = MudData { _armorTblTVar :: TVar (IntMap Armor)
, _clothingTblTVar :: TVar (IntMap Clothing)
, _coinsTblTVar :: TVar (IntMap Coins)
......等等。 (我使用镜头,因此是下划线。)
某些功能需要某些IntMap
s,而其他功能需要其他功能。因此,让IntMap
中的每个TVar
提供粒度。
但是,我的代码中出现了一种模式。在处理播放器命令的函数中,我需要读取(有时稍后写入)STM monad中的TVar
s。因此,这些函数最终在其where
块中定义了STM帮助程序。这些STM助手通常在其中有很多readTVar
个操作,因为大多数命令需要访问少数IntMap
个。此外,给定命令的函数可以调用许多纯辅助函数,这些函数也需要部分或全部IntMap
s。因此,这些纯粹的辅助函数最终会占用大量参数(有时超过10个)。
所以,我的代码变得越来越糟糕了#34;有很多readTVar
个表达式和函数,需要大量的参数。以下是我的问题:这是代码味道吗?我错过了一些可以使我的代码更优雅的抽象吗?有没有更理想的方法来构建我的数据/代码?
谢谢!
答案 0 :(得分:21)
此问题的解决方案是更改纯辅助函数。我们并不真的希望它们是纯粹的,我们希望泄露出一个副作用 - 无论它们是否读取特定的数据。
让我们说我们有一个只使用衣服和硬币的纯粹功能:
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
s中,另一个不包含在其中。我们可以修改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
的{{1}}版本与我们刚为MudData TVar
编写的纯版本几乎完全相同。它们仅根据引用的类型(MudData Identity
与TVar
)不同,我们使用什么函数从引用中获取值(Identity
vs readTVar
),以及如何返回结果(在runIdentity
中或作为普通值)。如果可以使用相同的功能来提供两者,那将是很好的。我们将提取两个函数之间的共同点。为此,我们将为STM
引入类型类MonadReadRef r m
,我们可以从中读取某种类型的引用。 Monad
是引用的类型,r
是从引用中获取值的函数,readRef
是返回结果的方式。以下m
与MonadRef
中的ref-fd类密切相关。
MonadReadRef
只要代码在所有{-# LANGUAGE FunctionalDependencies #-}
class Monad m => MonadReadRef r m | m -> r where
readRef :: r a -> m a
上进行参数化,它就是纯粹的。对于MonadReadRef r m
中保存的普通值,我们可以通过以下MonadReadRef
实例运行它来查看此内容。 Identity
中的id
与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
个实例时,我们可以使用这些&#34; pure&#34; STM
中的计算,但泄漏了读取STM
的副作用。
TVar
答案 1 :(得分:15)
是的,这显然会使您的代码变得复杂,并且会使用大量的样板详细信息来填充重要的代码。具有4个以上参数的函数是问题的标志。
我问这个问题:你真的通过单独TVar
来获得任何收益吗?不是premature optimization的情况吗?在做出这样的设计决策之前,在多个单独的TVar
之间拆分您的数据结构之前,我绝对会做一些测量(参见criterion)。您可以创建一个样本测试,对预期的并发线程数和数据更新频率进行建模,并通过将多个TVar
s与单个IORef
与STM
进行比较来检查您实际获得或失去的内容。< / p>
请记住:
STM
事务中有多个线程竞争公共锁,则事务可以在成功完成之前多次重新启动。所以在某些情况下,拥有多个锁实际上会让事情变得更糟。IORef
。它的原子操作速度非常快,可以弥补单个中央锁定。IORef
或IORef
事务是非常困难的。原因是懒惰:你只需要在这样的交易中创建thunk,而不是评估它们。对于单个原子TVar
尤其如此。在这样的事务之外评估thunk(通过检查它们的线程,或者你可以决定在某些时候强制它们,如果你需要更多的控制;这在你的情况下是可取的,就像你的系统在没有任何人观察它的情况下进化一样,你可以很容易地累积未评估的thunk。)如果事实证明拥有多个STM
确实至关重要,那么我可能会在自定义monad中编写所有代码(正如@Cirdec在我写答案时所描述的那样),其中实现将隐藏在主代码中,并且将提供用于读取(并且可能还写入)部分状态的功能。然后,它将作为单个{{1}}交易运行,只读取和编写所需的内容,并且您可以使用纯版本的monad进行测试。