У меня есть MonadReader
, который генерирует данные для приложения, над которым я работаю. Основная монада здесь генерирует данные на основе некоторых переменных среды. Монада генерирует данные, выбирая одну из нескольких других монад для запуска в зависимости от среды. Мой код выглядит примерно так, как показано ниже, где mainMonad
является основной монадой:
data EnvironmentData = EnvironmentA | EnvironmentB
type Environment = (EnvironmentData, Integer)
mainMonad ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
mainMonad = do
env <- ask
case env of
EnvironmentA -> monadA
EnvironmentB -> monadB
monadA ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadA = do
...
result <- helperA
result <- helper
...
monadB ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadB = do
start <- local (set _1 EnvironmentA) monadA
...
result <- helper
...
helperA ::
( MonadReader Environment m
, MonadRandom m
)
=> m String
helperA = do
...
helper ::
( MonadReader Environment m
, MonadRandom m
)
=> m String
helper = do
...
Примечательные вещи здесь:
- У нас есть главная монада (
mainMonad
), которая одновременно является иMonadReader Environment
, иMonadRandom
. - Основная монада обращается к подчиненным монадам
monadA
иmonadB
того же типа. - У нас есть четвертая монада, которая служит помощником для
monadA
иmonadB
. monadB
обращается кmonadA
(но используетlocal
для изменения окружения)
Самое главное:
- Всякий раз, когда вызывается
monadA
илиhelperA
,EnvironmentData
- этоEnvironmentA
, а всякий раз, когдаmonadB
вызывается,EnvironmentData
- этоEnvironmentB
.
Моя кодовая база в значительной степени представляет собой увеличенную версию этого. Есть больше подчиненных монад (на данный момент их 12, но, вероятно, их будет больше в будущем), больше помощников, и мой тип EnvironmentData
немного сложнее (хотя мой Environment
почти идентичен).
Последний пункт важен, потому что EnvironmentData
используется в хелперах, а неправильное Environment
приведет к незначительным изменениям в результатах хелперов.
Теперь моя проблема заключается в том, что довольно легко пропустить local
в моем коде и просто вызвать монаду напрямую с неправильной средой. Я также боюсь вызывать монаду без использования local
, потому что я думаю, что она ожидает среду, которой не является. Это крошечные и легкие ошибки (я делал это уже несколько раз), и результаты этого часто довольно тонкие и довольно разнообразные. Это приводит к тому, что симптомы проблемы довольно трудно обнаружить с помощью модульного тестирования. Поэтому я хотел бы нацелиться на проблему напрямую. Моим первым побуждением было добавить в модульный тест пункт, который говорит что-то вроде:
Вызовите
mainMonad
и убедитесь, что в ходе его оценки у нас никогда не было вызова монады с неправильным окружением.
Таким образом, я могу обнаружить эти ошибки, не просматривая код очень тщательно. Теперь, подумав об этом некоторое время, я не придумал очень аккуратного способа сделать это. Я подумал о нескольких способах, которые работают, но я не совсем доволен:
1. Жесткий сбой при вызове с неправильной средой
Я мог бы исправить это, добавив условие в начало каждой монады, которая аварийно завершает работу, если обнаружит, что она вызывается с неправильной средой. Например:
monadA ::
( MonadReader m
)
=> m Type
monadA = do
env <- view _1 ask
case env of
EnvironmentA -> return ()
_ -> undefined
...
Сбой будет обнаружен во время модульного тестирования, и я обнаружу проблему. Однако это не идеально, поскольку я действительно предпочел бы, чтобы клиент испытывал небольшие проблемы, вызванные вызовом вещей с неправильной средой, а не серьезным сбоем в случае, если обработчик теста не улавливает проблему. Это похоже на ядерный вариант. Это не ужасно, но неудовлетворительно по моим меркам и худшее из трех.
2. Используйте безопасность типов
Я также попытался изменить типы monadA
и monadB
, чтобы monadA
нельзя было вызвать напрямую из monadB
или наоборот. Это очень удобно, поскольку обнаруживает проблемы во время компиляции. У этого есть проблема, связанная с тем, что это немного больно поддерживать, и это довольно сложно. Поскольку monadA
и monadB
могут иметь несколько общих монад типа (MonadReader m) => m Type
, каждая из них также должна быть поднята. На самом деле это в значительной степени гарантирует, что каждая линия теперь имеет лифт. Я не против решений, основанных на типах, но я не хочу тратить много времени только на поддержку модульного теста.
3. Переместите локальные жители внутрь объявления
Каждая монада с ограничением на EnvironmentData
может начинаться с шаблона, похожего на:
monadA ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadA = do
env <- view _1 <$> ask
case env of
EnvironmentA ->
...
_ ->
local (set _1 EnvironmentA) monadA
Это хорошо тем, что гарантирует, что все всегда вызывается в правильном окружении. Однако проблема в том, что он молча «исправляет» ошибки, чего не делают модульные тесты или проверки типов. Это только мешает мне забыть local
.
3.5. Удалите EnvironmentData
Этот в основном эквивалентен последнему, хотя, возможно, немного чище. Если я изменю тип monadA
и monadB
на
( MonadReader Integer m
, MonadRandom m
)
=> m Type
затем добавьте оболочку, используя runReaderT
withReaderT
(как предложено Даниэлем Вагнером ниже) для вызовов, поступающих от и к мои MonadReader Environment
с. Я не могу вызвать их с неправильным EnvironmentData
, так как нет данных среды. У этого есть в значительной степени точные проблемы последних.
Итак, есть ли способ убедиться, что мои монады всегда вызываются из правильной среды?
withReaderT
, хотя это потребует от вас выбора определенного стека монад (или, по крайней мере, его внешних битов доReaderT
), а не использования полиморфизма классов в стиле mtl. - person Daniel Wagner   schedule 23.07.2019MonadReader
). Можете ли вы опубликовать несколько более реалистичный пример игрушки, который компилирует и иллюстрирует то, что вы пытаетесь сделать? - person K. A. Buhr   schedule 23.07.2019MonadReader
), а не конкретные типы монад? Я знаю, что некоторые люди считают это лучшей практикой, но есть ли у вас реальная необходимость запускать этот код полиморфно над несколькими конкретными монадами? - person K. A. Buhr   schedule 25.07.2019MonadRandom
, а не конкретную среду. Это связано с тем, что обработчик теста используетGen
в качестве экземпляра, а само приложение используетIO
. - person Éamonn Olive   schedule 25.07.2019