如何使用 QuickCheck 为 StateT 编写测试

How to write a test for StateT using QuickCheck

StateT 在Control.Monad.Trans.State.Lazy

里面的功能和m更高级的东西很难

{-# LANGUAGE FlexibleContexts #-}
import Test.QuickCheck

newtype StateT s m a = StateT { runStateT :: s -> m (a,s) }
instance (CoArbitrary s, Arbitrary s, Arbitrary a) => 
    Arbitrary (StateT s (Maybe) a) where -- doesn't quite work
    arbitrary = undefined

我想这样做的原因是因为我想使用 QuickCheck 检查我为 StateT 编写的应用实例(用于练习)是否正确

编辑: 好的,这是我要测试的实例(应该是不正确的)

instance (Monad m) => Applicative (StateT s m) where
    pure x = StateT (\s -> (\a -> (a, s)) <$> pure x)
    StateT smfs <*> StateT smas = StateT $ \s -> liftA2 (\ (f, s) (a, _) -> (f a, s)) (smfs s) (smas s)

你的问题很有意思。确实,使用 QuickCheck 来验证 functor/apllicative/monad 法则对于 StateT monad 转换器来说会非常好。因为这是 QuickCheck 最有用的应用之一。

但是为 StateT 编写 Arbitrary 实例并不简单。这是可能的。但实际上并没有任何利润。您应该以某种巧妙的方式使用 CoArbitrary 类型 class。还有一些扩展。 this blog post 中描述了这个想法。拥有 a -> bCoArbitrary 实例,您可以轻松地为 StateT.

创建 Arbitrary 实例
instance ( CoArbitrary s
         , Arbitrary s
         , Arbitrary a
         , Arbitrary (m a)
         , Monad m
         ) => Arbitrary (StateT s m a)
  where
    arbitrary = StateT <$> promote (\s -> fmap (,s) <$> arbitrary)

然后你可以生成状态:

ghci> (`runStateT` 3) <$> generate (arbitrary @(StateT Int Maybe Bool))
Just (True,3)

你甚至可以这样写 属性:

propStateFunctorId :: forall m s a .
                      ( Arbitrary s
                      , Eq (m (a, s))
                      , Show s
                      , Show (m (a, s))
                      , Functor m
                      )
                   => StateT s m a -> Property
propStateFunctorId st = forAll arbitrary $ \s -> 
                            runStateT (fmap id st) s === runStateT st s

但是你不能 运行 这个 属性 因为它需要 instance Show 用于 StateT 而你不能为它编写合理的实例 :( 这就是如何QuickCheck 有效。它应该打印失败的反例。如果您不知道哪个测试失败,那么知道 100 项测试通过和 1 项测试失败这一事实对您没有真正的帮助。这不是编程竞赛 :)

ghci> quickCheck (propStateFunctorId @Maybe @Int @Bool) 
<interactive>:68:1: error:
    • No instance for (Show (StateT Int Maybe Bool))
        arising from a use of ‘quickCheck’

因此,与其生成任意 StateT,不如生成 sa,然后检查属性。

prop_StateTFunctorId :: forall s a .
                      ( Arbitrary s
                      , Arbitrary a
                      , Eq a
                      , Eq s
                      )
                     => s -> a -> Bool
prop_StateTFunctorId s a = let st = pure a
                           in runStateT @_ @Maybe (fmap id st) s == runStateT st s

ghci> quickCheck (prop_StateTFunctorId @Int @Bool) 
+++ OK, passed 100 tests.

这种方法不需要掌握一些高级技能。