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

QuickCheck: как использовать проверку целостности для предотвращения забытых конструкторов типа суммы

У меня есть тип данных Haskell, например

data Mytype
  = C1
  | C2 Char
  | C3 Int String

Если я case на a Mytype и забудьте обработать один из случаев, GHC дает мне предупреждение (проверка полноты).

Теперь я хочу написать экземпляр QuickCheck Arbitrary для генерации MyTypes, например:

instance Arbitrary Mytype where
  arbitrary = do
    n <- choose (1, 3 :: Int)
    case n of
      1 -> C1
      2 -> C2 <$> arbitrary
      3 -> C3 <$> arbitrary <*> someCustomGen

Проблема заключается в том, что я могу добавить новую альтернативу Mytype и забыть обновить экземпляр Arbitrary, поэтому мои тесты не тестируют эту альтернативу.

Я хотел бы найти способ использования проверки GHC, чтобы напомнить мне о забытых случаях в моем произвольном экземпляре.

Лучшее, что я придумал, это

arbitrary = do
  x <- elements [C1, C2 undefined, C3 undefined undefined]
  case x of
    C1     -> C1
    C2 _   -> C2 <$> arbitrary
    C3 _ _ -> C3 <$> arbitrary <*> someCustomGen

Но это не очень элегантно.

Я интуитивно чувствую, что на это нет 100% чистого решения, но будет признателен за все, что уменьшит вероятность забыть такие случаи - особенно в большом проекте, где разделены код и тесты.

4b9b3361

Ответ 1

Здесь я использую неиспользуемую переменную _x. Однако это не очень элегантно, чем ваше решение.

instance Arbitrary Mytype where
  arbitrary = do
    let _x = case _x of C1 -> _x ; C2 _ -> _x ; C3 _ _ -> _x
    n <- choose (1, 3 :: Int)
    case n of
      1 -> C1
      2 -> C2 <$> arbitrary
      3 -> C3 <$> arbitrary <*> someCustomGen

Конечно, нужно сохранить последний case когерентный с фиктивным определением _x, поэтому он не является полностью DRY.

В качестве альтернативы, можно использовать Template Haskell для создания команды утверждения времени компиляции, чтобы конструкторы в Data.Data.dataTypeOf были ожидаемыми. Это утверждение должно быть согласовано с экземпляром Arbitrary, поэтому это также не является полностью сухим.

Если вам не нужны специальные генераторы, я считаю, что Data.Data может быть использован для генерации экземпляров Arbitrary через Template Haskell (я думаю, что видел какой-то код, выполняющий именно это, но я не могу вспомнить, где). Таким образом, нет возможности, чтобы экземпляр мог пропустить конструктор.

Ответ 2

Я реализовал решение с TemplateHaskell, вы можете найти прототип в https://gist.github.com/nh2/d982e2ca4280a03364a8. С этим вы можете написать:

instance Arbitrary Mytype where
  arbitrary = oneof $(exhaustivenessCheck ''Mytype [|
      [ pure C1
      , C2 <$> arbitrary
      , C3 <$> arbitrary <*> arbitrary
      ]
    |])

Он работает следующим образом: вы даете ему имя типа (например, ''Mytype) и выражение (в моем случае список arbitrary style Gen s). Он получает список всех конструкторов для этого имени типа и проверяет, содержит ли выражение все эти конструкторы хотя бы один раз. Если вы просто добавили конструктор, но забыли добавить его в произвольный экземпляр, эта функция предупредит вас во время компиляции.

Вот как это реализовано с TH:

exhaustivenessCheck :: Name -> Q Exp -> Q Exp
exhaustivenessCheck tyName qList = do
  tyInfo <- reify tyName
  let conNames = case tyInfo of
        TyConI (DataD _cxt _name _tyVarBndrs cons _derives) -> map conNameOf cons
        _ -> fail "exhaustivenessCheck: Can only handle simple data declarations"

  list <- qList
  case list of
    [email protected](ListE l) -> do
      -- We could be more specific by searching for `ConE`s in `l`
      let cons = toListOf tinplate l :: [Name]
      case filter (`notElem` cons) conNames of
        [] -> return input
        missings -> fail $ "exhaustivenessCheck: missing case: " ++ show missings
    _ -> fail "exhaustivenessCheck: argument must be a list"

Я использую GHC.Generics, чтобы легко пересечь дерево синтаксиса Exp: С toListOf tinplate exp :: [Name] (от lens) я могу легко найти все Name в целом Exp.

Я был удивлен, что типы из Language.Haskell.TH не имеют экземпляров Generic, и ни один из них (с текущим GHC 7.8) не выполняет Integer или Word8 - Generic экземпляры для них, потому что они появляются в Exp. Поэтому я добавил их как сиротские экземпляры (для большинства вещей StandaloneDeriving делает это, но для примитивных типов, таких как Integer мне приходилось копировать экземпляры в качестве Int).

Решение не является совершенным, потому что оно не использует проверку целостности, как case, но, как мы согласны, это невозможно при пребывании DRY, и это TH-решение DRY.

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

Ответ 3

Вы хотите, чтобы ваш код действовал определенным образом; самый простой способ проверить поведение кода - проверить его.

В этом случае желаемое поведение заключается в том, что каждый конструктор получает разумное покрытие в тестах. Мы можем проверить это с помощью простого теста:

allCons xs = length xs > 100 ==> length constructors == 3
             where constructors = nubBy eqCons xs
                   eqCons  C1       C1      = True
                   eqCons  C1       _       = False
                   eqCons (C2 _)   (C2 _)   = True
                   eqCons (C2 _)    _       = False
                   eqCons (C3 _ _) (C3 _ _) = True
                   eqCons (C3 _ _)  _       = False

Это довольно наивно, но это хороший первый выстрел. Его преимущества:

  • eqCons вызовет предупреждение об исчерпании, если новые конструкторы будут добавлены, что вы хотите
  • Он проверяет, что ваш экземпляр обрабатывает все конструкторы, что вы хотите
  • Он также проверяет, что все конструкторы фактически сгенерированы с некоторой полезной вероятностью (в данном случае не менее 1%)
  • Он также проверяет, что ваш экземпляр можно использовать, например. не висеть

Его недостатки:

  • Требуется большое количество тестовых данных, чтобы отфильтровать их с длиной > 100
  • eqCons является довольно многословным, так как catch-all eqCons _ _ = False обходит проверку исчерпания
  • Использует магические числа 100 и 3
  • Не очень общий

Есть способы улучшить это, например. мы можем вычислить конструкторы с помощью модуля Data.Data:

allCons xs = sufficient ==> length constructors == consCount
             where sufficient   = length xs > 100 * consCount
                   constructors = length . nub . map toConstr $ xs
                   consCount    = dataTypeConstrs (head xs)

Это теряет время исчерпывающей проверки времени компиляции, но оно избыточно, пока мы регулярно проверяем и наш код становится более общим.

Если мы действительно хотим проверить исчерпываемость, есть несколько мест, где мы могли бы обучить его снова:

allCons xs = sufficient ==> length constructors == consCount
             where sufficient   = length xs > 100 * consCount
                   constructors = length . nub . map toConstr $ xs
                   consCount    = length . dataTypeConstrs $ case head xs of
                                                                  [email protected](C1)     -> x
                                                                  [email protected](C2 _)   -> x
                                                                  [email protected](C3 _ _) -> x

Обратите внимание, что мы используем consCount для полного устранения магии 3. Магия 100 (определяющая минимальную требуемую частоту конструктора) теперь масштабируется с помощью consCount, но для этого требуется еще больше тестовых данных!

Мы можем решить это довольно легко, используя newtype:

consCount = length (dataTypeConstrs C1)

newtype MyTypeList = MTL [MyType] deriving (Eq,Show)

instance Arbitrary MyTypeList where
  arbitrary = MTL <$> vectorOf (100 * consCount) arbitrary
  shrink (MTL xs) = MTL (shrink <$> xs)

allCons (MTL xs) = length constructors == consCount
                   where constructors = length . nub . map toConstr $ xs

Мы можем поставить простую проверку полноты где-нибудь, если хотите, например.

instance Arbitrary MyTypeList where
  arbitrary = do x <- arbitrary
                 MTL <$> vectorOf (100 * consCount) getT
              where getT = do x <- arbitrary
                              return $ case x of
                                            C1     -> x
                                            C2 _   -> x
                                            C3 _ _ -> x
  shrink (MTL xs) = MTL (shrink <$> xs)