[英]Haskell code littered with TVar operations and functions taking many arguments: code smell?
我正在Haskell中编写MUD服务器(MUD =多用户地牢:基本上是一个多用户文本冒险/角色扮演游戏)。 游戏世界数据/状态在大约15种不同的IntMap
表示。 我的monad变换器堆栈看起来像这样: ReaderT MudData IO
,其中MudData
类型是包含IntMap
的记录类型,每个都在自己的TVar
(我使用STM进行并发):
data MudData = MudData { _armorTblTVar :: TVar (IntMap Armor)
, _clothingTblTVar :: TVar (IntMap Clothing)
, _coinsTblTVar :: TVar (IntMap Coins)
...等等。 (我正在使用镜头,因此是下划线。)
有些函数需要某些IntMap
,而其他函数需要其他函数。 因此,将每个IntMap
放在其自己的TVar
可提供粒度。
但是,我的代码中出现了一种模式。 在处理播放器命令的函数中,我需要在STM monad中读取(有时稍后写入)我的TVar
。 因此,这些函数最终在其where
块中定义了STM助手。 这些STM助手通常在其中有相当多的readTVar
操作,因为大多数命令需要访问少数IntMap
。 此外,给定命令的函数可以调用许多纯辅助函数,这些函数也需要部分或全部IntMap
。 因此,这些纯辅助函数有时会占用大量参数(有时超过10)。
因此,我的代码变得“乱七八糟”,其中包含大量带有大量参数的readTVar
表达式和函数。 以下是我的问题:这是代码味道吗? 我错过了一些可以使我的代码更优雅的抽象吗? 有没有更理想的方法来构建我的数据/代码?
谢谢!
这个问题的解决方案是改变纯辅助函数。 我们并不真的希望它们是纯粹的,我们想要泄漏一个副作用 - 无论它们是否读取特定的数据。
假设我们有一个仅使用衣服和硬币的纯功能:
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 TVar
恢复原始的MudData
。 我们可以通过将字段包装在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 ...
这个用于MudData TVar
STM
版本与我们刚为MudData Identity
编写的纯版本MudData Identity
。 它们仅根据引用的类型( TVar
与Identity
)不同,我们使用什么函数从引用中获取值( readTVar
与runIdentity
),以及返回结果的方式(在STM
或作为普通值)。 如果可以使用相同的功能来提供两者,那将是很好的。 我们将提取两个函数之间的共同点。 为此,我们将为Monad
引入一个类型MonadReadRef rm
,我们可以从中读取某种类型的引用。 r
是引用的类型, readRef
是从引用中获取值的函数, m
是返回结果的方式。 以下MonadReadRef
与ref-fd中的MonadRef
类密切相关。
{-# LANGUAGE FunctionalDependencies #-}
class Monad m => MonadReadRef r m | m -> r where
readRef :: r a -> m a
只要代码在所有MonadReadRef rm
参数化,它就是纯粹的。 我们可以通过使用以下MonadReadRef
实例运行它来MonadReadRef
Identity
中MonadReadRef
的普通值。 该id
在readRef = id
是一样的return . runIdentity
return . runIdentity
。
instance MonadReadRef Identity Identity where
readRef = id
我们将根据MonadReadRef
重写moreVanityThanWealth
的MonadReadRef
。
moreVanityThanWealth :: MonadReadRef r m => MudData r -> m Bool
moreVanityThanWealth md =
do
clothing <- readRef . _clothingTbl $ md
coins <- readRef . _coinsTbl $ md
return ...
当我们添加一个MonadReadRef
实例TVar
以s STM
,我们可以使用这些“纯”的计算STM
但泄漏的副作用,其中TVar
小号宣读。
instance MonadReadRef TVar STM where
readRef = readTVar
是的,这显然会使您的代码变得复杂,并使重要的代码与许多样板详细信息混杂在一起。 具有4个以上参数的函数是问题的标志。
我会问这样一个问题: 你是否通过单独的TVar
获得了什么? 是不是过早优化的情况? 在做出这样的设计决定之前,在多个独立的TVar
分割您的数据结构之前,我肯定会做一些测量(参见标准 )。 您可以创建一个样本测试,对预期的并发线程数和数据更新频率进行建模,并通过将多个TVar
与单个IORef
对比IORef
检查您真正获得或失去的是IORef
。
记住:
STM
事务中有多个线程竞争公共锁,则事务可以在成功完成之前多次重新启动。 所以在某些情况下,拥有多个锁实际上会使事情变得更糟。 IORef
。 它的原子操作非常快,可以补偿单个中央锁定。 STM
或IORef
事务是非常困难的。 原因是懒惰:你只需要在这样的交易中创建thunk,而不是评估它们。 对于单个原子IORef
尤其IORef
。 在这样的事务之外评估thunk(通过检查它们的线程,或者你可以决定在某些时候强制它们,如果你需要更多的控制;这在你的情况下是可取的,就像你的系统在没有任何人观察它的情况下进化一样,你很容易积累未评估的thunk)。 如果事实证明拥有多个TVar
确实至关重要,那么我可能会在自定义monad中编写所有代码(正如@Cirdec在我编写答案时所描述的那样),其实现将隐藏在主代码中,并且它将提供用于阅读(也可能还写作)州的一部分的功能。 然后它将作为单个STM
事务运行,只读取和写入所需的内容,并且您可以使用纯版本的monad进行测试。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.