2015-03-07 4 views
13

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

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. Кроме того, функция для данной команды может вызывать несколько чистых вспомогательных функций, которые также нуждаются в некоторых или всех IntMap. Таким образом, эти чистые вспомогательные функции в конечном итоге принимают множество аргументов (иногда более 10).

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

Спасибо!

+1

Вы передаете много «IntMap' вместо« MudData »? Будет ли последний вариант? Трудно сказать, не видя какого-то кода - можете ли вы поделиться небольшим фрагментом, показывающим случай, когда помощнику нужно так много аргументов? – chi

+1

Да, это определенно запах кода. Нет, я не знаю, как правильно это исправить. – dfeuer

+0

Я не очень хорошо знаком с TVar, но разве вы не можете его разложить? т.е. удалить все TVar из MUDData и использовать (TVar MUDData), где это необходимо? – mb14

ответ

14

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

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

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

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

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

+3

Благодарим вас за ваш вдумчивый ответ. Я думаю, что ты действительно ударил ноготь по голове: я не основывал свое решение использовать несколько ТВАР на любом бенчмарке. По правде говоря, в моей игре, вероятно, никогда не будет достаточно одновременных игроков, чтобы создать серьезное соперничество над одним замком. Тем не менее, я хотел сделать что-то «наилучшим образом» и написать сервер, который является масштабируемым. Но «лучший способ», который я себе представлял, в конечном итоге кусает меня. Возможно, я просто усвоил еще один урок. –

20

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

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

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 будет иметь слегка необычный вид (* -> *) -> *, который тесно связан с объективами, но не имеет большой поддержки библиотеки. Я называю этот шаблон a Модель.

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 от ref-fd.

{-# 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 s.

instance MonadReadRef TVar STM where 
    readRef = readTVar 
+1

Вау! Спасибо, что нашли время, чтобы написать подробный ответ. Я получаю общую идею, но мне нужно будет изучить, как работает абстракция и играть с каким-то кодом, чтобы полностью понять это. Но это аккуратно; Я бы никогда не подумал об этом. –

 Смежные вопросы

  • Нет связанных вопросов^_^