簡體   English   中英

Haskell代碼散落着TVar操作和函數帶來許多爭論:代碼味道?

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

MudDataMudData'幾乎相同。 其中一個將其田地包裹在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 它們僅根據引用的類型( TVarIdentity )不同,我們使用什么函數從引用中獲取值( readTVarrunIdentity ),以及返回結果的方式(在STM或作為普通值)。 如果可以使用相同的功能來提供兩者,那將是很好的。 我們將提取兩個函數之間的共同點。 為此,我們將為Monad引入一個類型MonadReadRef rm ,我們可以從中讀取某種類型的引用。 r是引用的類型, readRef是從引用中獲取值的函數, m是返回結果的方式。 以下MonadReadRefref-fd中MonadRef類密切相關。

{-# LANGUAGE FunctionalDependencies #-}

class Monad m => MonadReadRef r m | m -> r where
    readRef :: r a -> m a

只要代碼在所有MonadReadRef rm參數化,它就是純粹的。 我們可以通過使用以下MonadReadRef實例運行它來MonadReadRef IdentityMonadReadRef的普通值。 idreadRef = id是一樣的return . runIdentity return . runIdentity

instance MonadReadRef Identity Identity where
    readRef = id

我們將根據MonadReadRef重寫moreVanityThanWealthMonadReadRef

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 它的原子操作非常快,可以補償單個中央鎖定。
  • 在Haskell中,純函數很長時間地阻塞原子STMIORef事務是非常困難的。 原因是懶惰:你只需要在這樣的交易中創建thunk,而不是評估它們。 對於單個原子IORef尤其IORef 在這樣的事務之外評估thunk(通過檢查它們的線程,或者你可以決定在某些時候強制它們,如果你需要更多的控制;這在你的情況下是可取的,就像你的系統在沒有任何人觀察它的情況下進化一樣,你很容易積累未評估的thunk)。

如果事實證明擁有多個TVar確實至關重要,那么我可能會在自定義monad中編寫所有代碼(正如@Cirdec在我編寫答案時所描述的那樣),其實現將隱藏在主代碼中,並且它將提供用於閱讀(也可能還寫作)州的一部分的功能。 然后它將作為單個STM事務運行,只讀取和寫入所需的內容,並且您可以使用純版本的monad進行測試。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM