Подтвердить что ты не робот

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

Я пишу MUD-сервер в Haskell (MUD = Multi User Dungeon: в основном, многопользовательская текстовая приключенческая/ролевая игра). Данные/состояние игрового мира представлены примерно в 15 различных IntMap s. Модный блок трансформатора выглядит следующим образом: ReaderT MudData IO, где тип MudData - это тип записи, содержащий IntMap s, каждый в своем собственном TVar (я использую STM для concurrency):

data MudData = MudData { _armorTblTVar    :: TVar (IntMap Armor)
                       , _clothingTblTVar :: TVar (IntMap Clothing)
                       , _coinsTblTVar    :: TVar (IntMap Coins)

... и так далее. (Я использую объективы, поэтому подчеркивания.)

Некоторые функции нуждаются в определенных IntMap s, тогда как другим функциям нужны другие. Таким образом, наличие каждого IntMap в своем собственном TVar обеспечивает детализацию.

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

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

Спасибо!

4b9b3361

Ответ 1

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

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

Имейте в виду:

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

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

Ответ 2

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

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

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

{-# LANGUAGE FunctionalDependencies #-}

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

Пока код параметризуется по всем MonadReadRef r m s, он чист. Мы можем это увидеть, выполнив его со следующим экземпляром 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