DarkOtter のコメントでは、QuickCheckArbitrary
とCoArbitrary
クラスについて言及されていますが、これは確かに最初に試す必要があります。QuickCheck には次のインスタンスがあります。
instance (CoArbitrary a, Arbitrary b) => Arbitrary (a -> b) where ...
たまたま、昨日 QuickCheck のコードを読んで、これがどのように機能するかを理解していたので、覚えているうちに学んだことを共有できます。QuickCheck は、次のような型を中心に構築されています (これはまったく同じではありません)。
type Size = Int
-- | A generator for random values of type @a@.
newtype Gen a =
MkGen { -- | Generate a random @a@ using the given randomness source and
-- size.
unGen :: StdGen -> Size -> a
}
class Arbitrary a where
arbitrary :: a -> Gen a
最初の秘訣は、QuickCheck には次のように機能する関数があることです (実装方法を正確に理解していませんでした)。
-- | Use the given 'Int' to \"perturb\" the generator, i.e., to make a new
-- generator that produces different pseudorandom results than the original.
variant :: Int -> Gen a -> Gen a
次に、これを使用して、このCoArbitrary
クラスのさまざまなインスタンスを実装します。
class CoArbitrary a where
-- | Use the given `a` to perturb some generator.
coarbitrary :: a -> Gen b -> Gen b
-- Example instance: we just treat each 'Bool' value as an 'Int' to perturb with.
instance CoArbitrary Bool where
coarbitrary False = variant 0
coarbitrary True = variant 1
これらのピースが配置されたので、次のようにします。
instance (Coarbitrary a, Arbitrary b) => Arbitrary (a -> b) where
arbitrary = ...
実装は書きませんが、アイデアは次のとおりです。
- の
CoArbitrary
インスタンスa
と のArbitrary
インスタンスを使用して、型を持つb
関数を作成できます。\a -> coarbitrary a arbitrary
a -> Gen b
Gen b
は の newtype でStdGen -> Size -> b
あるため、型a -> Gen b
は と同形であることを思い出してくださいa -> StdGen -> Size -> b
。
- 後者の型の任意の関数を取り、引数の順序を入れ替えて type の関数を返す関数を自明に書くことができます
StdGen -> Size -> a -> b
。
- この再配置された型は に同形
Gen (a -> b)
なので、ほら、再配置された関数を にパックし、Gen
ランダム関数ジェネレーターを取得しました!
QuickCheck のソースを読んで、これを自分で確認することをお勧めします。これに取り組むと、速度を低下させる可能性のある 2 つの追加の詳細に遭遇するだけです。まず、HaskellRandomGen
クラスには次のメソッドがあります。
-- | The split operation allows one to obtain two distinct random generators.
split :: RandomGen g => g -> (g, g)
この操作は のMonad
インスタンスで使用され、Gen
かなり重要です。ここでのトリックの 1 つは、StdGen
が純粋な疑似乱数ジェネレーターであることです。仕組みとしては、ジェネレーターを摂動するGen (a -> b)
の可能な値ごとに、その摂動されたジェネレーターを使用して結果を生成しますが、摂動されたジェネレーターの状態を進めることはありません。基本的に、生成された関数は疑似乱数シードに対するクロージャーであり、いくつかでそれを呼び出すたびに、その固有のものを使用して新しいシードを決定論的に作成し、それを使用して依存する aと隠れたシードを決定論的に生成します。a
b
b
a -> b
a
a
b
a
省略形Seed -> a -> b
は、多かれ少なかれ何が起こっているかを要約しています。疑似乱数関数は、b
疑似乱数シードと からを生成するための規則ですa
。これは、命令型のステートフルな乱数ジェネレーターでは機能しません。
(a -> StdGen -> Size -> b) -> StdGen -> Size -> a -> b
2 つ目:上記で説明したように関数を直接持つ代わりに、QuickCheck コードには があります。promote :: Monad m => m (Gen a) -> Gen (m a)
これは、それを任意の に一般化したものMonad
です。m
の関数インスタンスがいつMonad
とpromote
一致する(a -> Gen b) -> Gen (a -> b)
ので、上でスケッチしたものとまったく同じです。