モジュールTest.QuickCheck.Arbitrary
とTest.QuickCheck.Gen
.
1 つのパラメーターのみ
引数が 1 つだけの関数に必要なものを提供する簡単なコードを次に示します。
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Gen
import System.Random
randomEvaluate :: (Arbitrary a, Show a, Show b) => (a -> b) -> IO (String, String)
randomEvaluate f = do
stdGen <- newStdGen
let x = unGen arbitrary stdGen 1000
let y = f x
return (show x, show y)
そして、ここでそれを実際に見ることができます:
*Main> randomEvaluate (\(a,b) -> a + b)
("(-292,-655)","-947")
*Main> randomEvaluate (\(a,b) -> a + b)
("(586,-905)","-319")
*Main> randomEvaluate (\(a,b) -> a + b)
("(547,-72)","475")
ご覧のとおり、アンカリー化すると、複数の引数を持つ関数で使用できます。それが十分でない場合は、少し難しくなりますが、型クラスの策略で可能になるはずです。
複数のパラメーター、戻り値の型が明示的にマークされている
これは、関数の戻り値を newtype でラップするために「のみ」を必要とするアプローチです。(これは、Haskell98 以外の機能では回避できる可能性があります):
class RandEval a where
randomEvaluate :: StdGen -> a -> ([String], String)
newtype Ret a = Ret a
instance Show a => RandEval (Ret a) where
randomEvaluate _ (Ret x) = ([], show x)
instance (Show a, Arbitrary a, RandEval b) => RandEval (a -> b) where
randomEvaluate stdGen f = (show x : args, ret)
where (stdGen1, stdGen2) = split stdGen
x = unGen arbitrary stdGen1 1000
(args, ret) = randomEvaluate stdGen2 (f x)
doRandomEvaluate :: RandEval a => a -> IO ([String], String)
doRandomEvaluate f = do
stdGen <- newStdGen
return $ randomEvaluate stdGen f
ここで実際にそれを見てください:
*Main> doRandomEvaluate (\a b -> Ret (a && b))
(["False","True"],"False")
*Main> doRandomEvaluate (\a b -> Ret (a + b))
(["944","758"],"1702")
*Main> doRandomEvaluate (\a b c -> Ret (a + b + c))
(["-274","413","865"],"1004")
*Main> doRandomEvaluate (\a b c d -> Ret (a + b + c + d))
(["-61","-503","-704","-877"],"-2145")
言語拡張機能付きの複数のパラメーター
戻り値を明示的にマークする必要があるのも望ましくない場合、これは機能しますが、言語拡張を使用します。
{-# LANGUAGE FlexibleInstances, UndecidableInstances, OverlappingInstances #-}
import Test.QuickCheck.Arbitrary
import Test.QuickCheck.Gen
import System.Random
import Control.Arrow
class RandEval a where
randomEvaluate :: StdGen -> a -> ([String], String)
instance (Show a, Arbitrary a, RandEval b) => RandEval (a -> b) where
randomEvaluate stdGen f = first (show x:) $ randomEvaluate stdGen2 (f x)
where (stdGen1, stdGen2) = split stdGen
x = unGen arbitrary stdGen1 1000
instance Show a => RandEval a where
randomEvaluate _ x = ([], show x)
doRandomEvaluate :: RandEval a => a -> IO ([String], String)
doRandomEvaluate f = do
stdGen <- newStdGen
return $ randomEvaluate stdGen f
そして、ここに投稿からの元のユースケースがあります:
*Main> doRandomEvaluate ( (+) :: Int -> Int -> Int )
(["-5998437593420471249","339001240294599646"],"-5659436353125871603")
しかし今では、GHC が重複するインスタンスをどのように解決するかについて気まぐれです。たとえば、ブール関数を表示するために、この素敵な (ただし、Haskell98 以外の) インスタンスを使用しても、次のようになります。
type BoolFun a = Bool -> a
instance Show a => Show (BoolFun a) where
show f = "True -> " ++ show (f True) ++ ", False -> " ++ show (f False)
aBoolFun :: Bool -> BoolFun Bool
aBoolFun x y = x && y
で使用されているこのインスタンスは表示されませんdoRandomEvaluate
:
*Main> doRandomEvaluate aBoolFun
(["False","False"],"False")
元のソリューションでは、次のことを行います。
*Main> doRandomEvaluate (Ret . aBoolFun)
(["False"],"True -> False, False -> False")
*Main> doRandomEvaluate (Ret . aBoolFun)
(["True"],"True -> True, False -> False")
警告
ただし、これは滑りやすい斜面であることに注意してください。上記のコードを少し変更すると、GHC 7.6.1 で動作しなくなります (ただし、GHC 7.4.1 では動作します):
instance (Show a, Arbitrary a, RandEval b) => RandEval (a -> b) where
randomEvaluate stdGen f = (show x:args, ret)
where (stdGen1, stdGen2) = split stdGen
x = unGen arbitrary stdGen1 1000
(args, ret) = randomEvaluate stdGen2 (f x)
SPJ は、これが実際にはバグではない理由を説明しています。私にとっては、このアプローチが型クラスのハッカーを少し行き過ぎているという明確な兆候です。