Шаблон для универсального модульного теста реализации экземпляра класса типа в Haskell

Мне было интересно, существует ли известный шаблон для написания универсального кода модульного тестирования, целью которого является проверка (как черный ящик) различных экземпляров (реализации) класса типов. Например:

import Test.HUnit

class M a where
foo :: a -> String
cons :: Int -> a     -- some constructor

data A = A Int
data B = B Int

instance M A where
  foo _ = "foo"
  cons  = A

instance M B where
  foo _ = "bar"     -- implementation error
  cons  = B 

Я хотел бы написать функцию, tests возвращающую Test, с каким-то способом указать tests конкретный экземпляр, к которому применяется код. Я думал добавить tests к определению класса с реализацией по умолчанию (пока игнорируя проблему связи между тестируемым кодом и фактическим кодом), но я не могу просто иметь tests :: Test, и даже если я попробую tests:: a -> Test (поэтому придется искусственно передать конкретный элемент данного типа для вызова функции), я не могу понять, как ссылаться на cons и foo внутри кода (аннотации типов, такие как (cons 0) :: a, не годятся).

Предполагая, что вместо этого у меня есть class (Eq a) => M a where ..., с типами A и B, производными от Eq, я мог бы обмануть компилятор чем-то вроде (добавлено к определению M):

tests :: a -> Test
tests x = let 
            y = (cons 0)
            z = (x == y)       -- compiler now knows y :: a
          in
            TestCase (assertEqual "foo" (foo y)  "foo")

main = do
  runTestTT $ TestList
   [ tests (A 0)
   , tests (B 0)
   ]

Но мне все это очень некрасиво. Любое предложение тепло приветствуется


person Sven Williamson    schedule 05.05.2017    source источник


Ответы (1)


Прокси

В настоящее время наиболее распространенным способом сделать функцию полиморфной во «внутреннем» типе является передача Proxy. Proxy имеет единственный нулевой конструктор, такой как (), но его тип имеет фантомный тип. Это позволяет избежать передачи undefined или фиктивных значений. Затем Data.Proxy.asProxyTypeOf можно использовать в качестве аннотации.

tests :: M a => Proxy a -> Test
tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")

прокси

Мы также можем обобщить этот тип, так как Proxy на самом деле не требуется в качестве значения. Это просто способ сделать переменную типа однозначной. Однако вам нужно переопределить asProxyTypeOf. Это в основном вопрос стиля по сравнению с предыдущим. Возможность использовать больше значений в качестве потенциальных заместителей может сделать некоторый код более кратким, иногда за счет удобочитаемости.

-- proxy is a type variable of kind * -> *
tests :: M a => proxy a -> Test
tests a = TestCase (assertEqual "foo" (foo (cons 0 `asProxyTypeOf` a)) "foo")
  where
    asProxyTypeOf :: a -> proxy a -> a
    asProxyTypeOf = const

Переменные типа с областью действия

Функция asProxyTypeOf или ваш трюк (==) на самом деле являются продуктом невозможности сослаться на переменную типа из подписи. На самом деле это разрешено расширениями ScopedTypeVariables+RankNTypes.

Явная квантификация вводит переменную a в область видимости в теле функции.

tests :: forall a proxy. M a => proxy a -> Test
tests _ = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.

Без расширения ScopedTypeVariables вместо этого cons 0 :: a будет интерпретироваться как cons 0 :: forall a. a.

Вот как вы используете эти функции:

main = runTestTT $ TestList
  [ tests (Proxy :: Proxy A)
  , tests (Proxy :: Proxy B)
  ]

Тип приложений

Начиная с GHC 8, расширения AllowAmbiguousTypes+TypeApplications делают аргумент Proxy ненужным.

tests :: forall a. M a => Test
tests = TestCase (assertEqual "foo" (foo (cons 0 :: a)) "foo")  -- the "a" bound by the top-level signature.

main = runTestTT $ TestList
  [ tests @A
  , tests @B
  ]
person Li-yao Xia    schedule 05.05.2017
comment
Это здорово спасибо!! Я тщательно попробую все эти предложения, так как я ожидаю многому научиться на этом упражнении. - person Sven Williamson; 05.05.2017