[英]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.