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 arbitrarya -> 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と隠れたシードを決定論的に生成します。abba -> baaba
省略形Seed -> a -> bは、多かれ少なかれ何が起こっているかを要約しています。疑似乱数関数は、b疑似乱数シードと からを生成するための規則ですa。これは、命令型のステートフルな乱数ジェネレーターでは機能しません。
(a -> StdGen -> Size -> b) -> StdGen -> Size -> a -> b2 つ目:上記で説明したように関数を直接持つ代わりに、QuickCheck コードには があります。promote :: Monad m => m (Gen a) -> Gen (m a)これは、それを任意の に一般化したものMonadです。mの関数インスタンスがいつMonadとpromote一致する(a -> Gen b) -> Gen (a -> b)ので、上でスケッチしたものとまったく同じです。