QuickCheck で特定の文字からなるランダムな文字列の生成

DFA/NFA のテストとして、特定の文字からなるランダムな文字列をテストデータとしたい。例えば、アルファベットが文字 a と b のみからなるDFAに対しては、"a", "ab", "aaaababa" などの文字列を生成したい。
しばらく方法が分からず悩んでいたが以下のように listOf(または listOf1) と elements のくみあわせでできた。

rndString0 str = listOf1 (elements str)

*FA> :t listOf1
listOf1 :: Gen a -> Gen [a]
*FA> :t elements
elements :: [a] -> Gen a
*FA> :t rndString0
rndString0 :: [a] -> Gen [a]

以下のように使う。listOf だと空文字列も含まれるが、listOf1 だと1文字以上の文字列になるので、目的によって使い分ける。

*FA> sample (rndString0 "ab")
"b"
"bb"
"ab"
"baaa"
"bbbabb"
"bbabaaba"
"aaabba"
"aaaabababb"
"abaaabbbaaabab"
"ababbaaabaabbabb"
"baa"
*FA> sample (rndString0 [0,1,2,3,4,5])
[4]
[3]
[3,4,0,4]
[5,4,1,3,1]
[2,3]
[4,0,0,4,1,1,0,1,1]
[5,2,5,1,0,5,3]
[4,5,3,2,2,0,1,2,4,5]
[4,1,1,3,5]
[3,3,2,4,3,5,3,1,5,5,4,3,2]
[1,2,5,4,1,4,2,1,4,4,0,4,0,4,3,0,2,4]

randString0 をDFAのアルファベット用に修正して、DFA の(幾つかの)テストができる。例えば、DFAと等価な状態数最小のDFAは、任意の文字列について受理するかしないか、が等しくなければならない。具体的なオートマトン(ここでは正規表現 "a(a|b)*ab" と等価な dfa_aabab )を用いて以下のようなテストを走らせることができる。

prop_minDFA dfa = forAll (rndString dfa) 
                  (\str -> accept dfa str == accept (minimizeDFA dfa) str)

*FA> quickCheck (prop_minDFA dfa_aabab)
+++ OK, passed 100 tests.
*FA> 

いい感じ。

さらに、既存の、正しいことが分かっているHaskell正規表現と比較してみよう。Text.Regex.Posix をインポートして、以下のように書けた。

regexpMatch reg str = str R.=~ reg :: Bool

prop_DFAREG dfa reg = forAll (rndString dfa)
           (\str -> accept dfa str == regexpMatch reg str)

*FA> quickCheck (prop_DFAREG dfa_aabab "^a(a|b)*ab$")
+++ OK, passed 100 tests.

ここで、比較する正規表現は "a(a|b)*ab" では部分一致を許してしまうため、テストをパスしない。"^a(a|b)*ab$" としなくてはならない。