Код на Haskell полон операций и функций TVar, принимающих множество аргументов: запах кода?

Я пишу MUD-сервер на Haskell (MUD = Многопользовательская темница: в основном, многопользовательская текстовая приключенческая / ролевая игра). Данные / состояние игрового мира представлены примерно в 15 различных IntMaps. Мой монадный стек трансформеров выглядит так: 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 обеспечивает гранулярность.

Тем не менее, шаблон появился в моем коде. В функциях, которые обрабатывают команды игрока, мне нужно прочитать (а иногда и написать) TVarв монаде STM. Таким образом, эти функции заканчиваются тем, что в их where блоки. Эти помощники STM часто имеют немало readTVar операций в них, так как большинству команд требуется доступ к горстке IntMaps. Кроме того, функция для данной команды может вызывать ряд чисто вспомогательных функций, которым также необходимы некоторые или все IntMaps. Эти чистые вспомогательные функции, таким образом, иногда заканчиваются принятием множества аргументов (иногда более 10).

Итак, мой код стал "замусоренным" с большим количеством readTVar выражения и функции, которые принимают большое количество аргументов. Вот мои вопросы: это запах кода? Я пропускаю некоторую абстракцию, которая сделала бы мой код более элегантным? Есть ли более идеальный способ структурировать мои данные / код?

Спасибо!

2 ответа

Решение

Да, это, очевидно, делает ваш код сложным и загромождает важный код большим количеством шаблонных деталей. И функции с более чем 4 аргументами являются признаком проблем.

Я бы задал вопрос: вы действительно что-то получаете, имея отдельныйTVar s? Разве это не случай преждевременной оптимизации? Прежде чем принять такое дизайнерское решение, как разделение вашей структуры данных между несколькими отдельнымиTVars, я определенно сделал бы некоторые измерения (см. критерий). Вы можете создать пример теста, который моделирует ожидаемое количество одновременных потоков и частоту обновления данных и проверяет, что вы действительно получаете или теряете, имея несколькоTVars против одного противIORef,

Иметь ввиду:

  • Если есть несколько потоков, конкурирующих за общие блокировки вSTMтранзакции, транзакции могут быть перезапущены несколько раз, прежде чем им удастся успешно завершить. Поэтому при некоторых обстоятельствах наличие нескольких блокировок может на самом деле ухудшить ситуацию.
  • Если в конечном итоге требуется синхронизировать только одну структуру данных, вы можете рассмотреть возможность использования единойIORef вместо. Это атомарные операции очень быстрые, что может компенсировать наличие единого центрального замка.
  • В Haskell удивительно трудно для чистой функции блокировать атомарный STM или IORef сделка на долгое время. Причина в лени: вам нужно только создавать блоки в такой транзакции, а не оценивать их. Это верно, в частности, для одного атомного IORef, Thunks оцениваются вне таких транзакций (потоком, который их проверяет, или вы можете решить принудить их в какой-то момент, если вам нужен больший контроль; это может быть желательным в вашем случае, как если бы ваша система развивалась, и никто ее не наблюдал, Вы можете легко накапливать неоцененные бандиты).

Если окажется, что, имея несколько TVars действительно важен, тогда я, вероятно, написал бы весь код в пользовательской монаде (как описано @Cirdec, когда я писал свой ответ), реализация которой была бы скрыта от основного кода, и которая обеспечивала бы функции для чтения (и, возможно, также написание) части государства. Затем он будет запущен как единый STM транзакции, чтение и запись только того, что нужно, и вы можете иметь чистую версию монады для тестирования.

Решение этой проблемы заключается в изменении чисто вспомогательных функций. Мы на самом деле не хотим, чтобы они были чистыми, мы хотим исключить единственный побочный эффект - независимо от того, читают они определенные фрагменты данных или нет.

Допустим, у нас есть чистая функция, которая использует только одежду и монеты:

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 с 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 версия для MudData TVar почти так же, как чистая версия, которую мы только что написали для MudData Identity, Они отличаются только типом ссылки (TVar против Identity), какую функцию мы используем для получения значений из ссылок (readTVar против runIdentity) и как возвращается результат (в STM или в виде простого значения). Было бы хорошо, если бы для обеспечения обоих использовалась одна и та же функция. Мы собираемся извлечь то, что является общим между двумя функциями. Для этого мы введем класс типов MonadReadRef r m для Monad s мы можем прочитать некоторые ссылки из. r тип ссылки, readRef является функцией для получения значений из ссылок, и m как результат возвращается. Следующие MonadReadRef тесно связан с MonadRef класс от реф-фд.

{-# 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 в STM мы можем использовать эти "чистые" вычисления в STM но утечка побочный эффект которого TVar с были прочитаны.

instance MonadReadRef TVar STM where
    readRef = readTVar
Другие вопросы по тегам