Код на Haskell полон операций и функций TVar, принимающих множество аргументов: запах кода?
Я пишу MUD-сервер на Haskell (MUD = Многопользовательская темница: в основном, многопользовательская текстовая приключенческая / ролевая игра). Данные / состояние игрового мира представлены примерно в 15 различных IntMap
s. Мой монадный стек трансформеров выглядит так: 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
операций в них, так как большинству команд требуется доступ к горстке IntMap
s. Кроме того, функция для данной команды может вызывать ряд чисто вспомогательных функций, которым также необходимы некоторые или все IntMap
s. Эти чистые вспомогательные функции, таким образом, иногда заканчиваются принятием множества аргументов (иногда более 10).
Итак, мой код стал "замусоренным" с большим количеством readTVar
выражения и функции, которые принимают большое количество аргументов. Вот мои вопросы: это запах кода? Я пропускаю некоторую абстракцию, которая сделала бы мой код более элегантным? Есть ли более идеальный способ структурировать мои данные / код?
Спасибо!
2 ответа
Да, это, очевидно, делает ваш код сложным и загромождает важный код большим количеством шаблонных деталей. И функции с более чем 4 аргументами являются признаком проблем.
Я бы задал вопрос: вы действительно что-то получаете, имея отдельныйTVar
s? Разве это не случай преждевременной оптимизации? Прежде чем принять такое дизайнерское решение, как разделение вашей структуры данных между несколькими отдельнымиTVar
s, я определенно сделал бы некоторые измерения (см. критерий). Вы можете создать пример теста, который моделирует ожидаемое количество одновременных потоков и частоту обновления данных и проверяет, что вы действительно получаете или теряете, имея несколькоTVar
s против одного противIORef
,
Иметь ввиду:
- Если есть несколько потоков, конкурирующих за общие блокировки в
STM
транзакции, транзакции могут быть перезапущены несколько раз, прежде чем им удастся успешно завершить. Поэтому при некоторых обстоятельствах наличие нескольких блокировок может на самом деле ухудшить ситуацию. - Если в конечном итоге требуется синхронизировать только одну структуру данных, вы можете рассмотреть возможность использования единой
IORef
вместо. Это атомарные операции очень быстрые, что может компенсировать наличие единого центрального замка. - В Haskell удивительно трудно для чистой функции блокировать атомарный
STM
илиIORef
сделка на долгое время. Причина в лени: вам нужно только создавать блоки в такой транзакции, а не оценивать их. Это верно, в частности, для одного атомногоIORef
, Thunks оцениваются вне таких транзакций (потоком, который их проверяет, или вы можете решить принудить их в какой-то момент, если вам нужен больший контроль; это может быть желательным в вашем случае, как если бы ваша система развивалась, и никто ее не наблюдал, Вы можете легко накапливать неоцененные бандиты).
Если окажется, что, имея несколько TVar
s действительно важен, тогда я, вероятно, написал бы весь код в пользовательской монаде (как описано @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