Haskell代码散落着TVar操作和函数带来许多争论:代码味道?

时间:2015-03-07 17:59:48

标签: haskell stm tvar

我在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个表达式和函数,需要大量的参数。以下是我的问题:这是代码味道吗?我错过了一些可以使我的代码更优雅的抽象吗?有没有更理想的方法来构建我的数据/代码?

谢谢!

2 个答案:

答案 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  ...

MudDataMudData'几乎相同。其中一个将其字段包含在TVar s中,另一个不包含在其中。我们可以修改MudData,这样就需要一个额外的类型参数(种类* -> *)来包含字段。MudData会有一些不寻常的类型(* -> *) -> *,这与镜片密切相关,但没有太多的图书馆支持。我将此模式称为模型

data MudData f = MudData { _armorTbl    :: f (IntMap Armor)
                         , _clothingTbl :: f (IntMap Clothing)
                         , _coinsTbl    :: f (IntMap Coins)

我们可以使用MudData恢复原始MudData TVar。我们可以通过将字段包装在Identitynewtype 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 IdentityTVar)不同,我们使用什么函数从引用中获取值(Identity vs readTVar),以及如何返回结果(在runIdentity中或作为普通值)。如果可以使用相同的功能来提供两者,那将是很好的。我们将提取两个函数之间的共同点。为此,我们将为STM引入类型类MonadReadRef r m,我们可以从中读取某种类型的引用。 Monad是引用的类型,r是从引用中获取值的函数,readRef是返回结果的方式。以下mMonadRef中的ref-fd类密切相关。

MonadReadRef

只要代码在所有{-# LANGUAGE FunctionalDependencies #-} class Monad m => MonadReadRef r m | m -> r where readRef :: r a -> m a 上进行参数化,它就是纯粹的。对于MonadReadRef r m中保存的普通值,我们可以通过以下MonadReadRef实例运行它来查看此内容。 Identity中的idreadRef = 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与单个IORefSTM进行比较来检查您实际获得或失去的内容。< / p>

请记住:

  • 如果在STM事务中有多个线程竞争公共锁,则事务可以在成功完成之前多次重新启动。所以在某些情况下,拥有多个锁实际上会让事情变得更糟。
  • 如果最终只需要同步一个数据结构,则可以考虑使用单个IORef。它的原子操作速度非常快,可以弥补单个中央锁定。
  • 在Haskell中,纯函数长时间阻塞原子IORefIORef事务是非常困难的。原因是懒惰:你只需要在这样的交易中创建thunk,而不是评估它们。对于单个原子TVar尤其如此。在这样的事务之外评估thunk(通过检查它们的线程,或者你可以决定在某些时候强制它们,如果你需要更多的控制;这在你的情况下是可取的,就像你的系统在没有任何人观察它的情况下进化一样,你可以很容易地累积未评估的thunk。)

如果事实证明拥有多个STM确实至关重要,那么我可能会在自定义monad中编写所有代码(正如@Cirdec在我写答案时所描述的那样),其中实现将隐藏在主代码中,并且将提供用于读取(并且可能还写入)部分状态的功能。然后,它将作为单个{{1}}交易运行,只读取和编写所需的内容,并且您可以使用纯版本的monad进行测试。