人気ブログランキング | 話題のタグを見る

Eliza (6) applyRules

apllyRules のコードを再掲する。

applyRules q = head matchingRules
    where matchingRules = do (pats,resps) <- ruleData
                             pat <- pats
                             Just sub <- [matchSomewhere pat (" " ++ q)]
                             return $ map (doSub $ changePerspective sub) resps
          doSub sub ('_' : resp) = sub ++ resp
          doSub sub (c : resp)   = c : doSub sub resp
          doSub sub []             = []

局所定義の doSub が分かりやすそうなので先に読んでみる。doSub は再帰関数だ。1行目と3行目は初期条件 the base case で2行目が再帰的定義だ。1行目のパターンでは doSub の第2引数の文字列がアンダースコアの時に、アンダースコアを第1引き数に置き換える。2行目はアンダースコアーを一文字ずつ探索していく。3行目は第2引数が空リストになった時の処理をしている。要する第2引数の文字列中のアンダースコアを第1引き数の文字列で置き換える関数だ。

ghci で試してみた。

Prelude> let doSub sub ('_' : resp) = sub ++ resp; doSub sub (c : resp) = c : doSub sub resp; doSub sub [] = []
Prelude> doSub "SUB" "foo _ bar"
"foo SUB bar"

'_' が "SUB" で置き換えられている。こういう文字列処理は頻繁に出会うので doSub は汎用性がある。

doSub の機能が分かったので、matchingRules を解読してみる。matchingRules は do 記法で記述されているが、IO モナドのプログラムではなく、リストモナドのプログラムだ。したがって <- はリストから要素をひとつづつ取り出すという意味になる。リストなどで do 記法を使ったプログラムを次に示す。

Prelude> do x <- [1,2,3]; return (x * 2)
[2,4,6]

これは、map (*2) [1,2,3] と同じ意味になる。リストの全ての要素について処理を施すときにリストモナドを使う利点は、map 関数と同じことが複雑な処理にも使えるということだ。

<- の意味が分かったので mathcingRules を読んでみると、

(pats,resps) <- ruleData

でリストruleData からパターンのリスト pat と そのパターンに対する応答のリスト resps を取り出す。また、

pat <- pats

で、パターンのリストの要素をひとつ pat に取り出す。つぎに、

Just sub <- [matchSomewhere pat (" " ++ q)]

でコンソールから入力されたユーザの入力文字列からパターンに合致した部分 sub をとりだし、

return $ map (doSub $ changePerspective sub) resps

で、パターンに合致した部分の人称を変更した上でそれを resps のアンダースコア '_' 部分と置き換える。という動作を ruleData の要素について実行している。つまり、applyRules はユーザが入力した文字列のパターンを分析し、そのパターンに応じて人称を入れ替えたオウム返しの返答(のリスト)を作成しているということになる。

もう峠は超えたのであとは一気に行ってみる。

次の関数は conversation だ、乱数値の無限リストと端末からの入力を受け取り、ランダムに入力に対する応答を選び出す。注目しないといけないのは、convo が再帰的定義になっていることだ。それも初期条件 the base case がないので無限にループが続く。これでは、端末からコントロールDで入力を止められないのでいいのだろうかと思うが手抜きしたのだろう。

しかし、重要なのは端末からの入力を処理するようなループを、IOモナドではなく純粋関数の世界でも工夫すればプログラムすることができるということだ。

conversation rands input = unlines (convo (lines input) rands)
    where convo (line : rest) (r : randrest) =
              let answers = applyRules line in
               choice r answers : convo rest randrest
          choice r xs = xs!!(r `mod` (length xs))

最後はメインループだ。

main :: IO ()
main = do r <- getStdGen
          interact (conversation (randoms r))

とくに解読するようなものもないが、randoms は乱数の無限リストを発生させる関数だ。ghc で試すと次のようになる。

Prelude> :m System.Random
Prelude System.Random> do r <- getStdGen; return $ take 10 (randoms r)
[-1707593484,-818636185,1280678006,-604207182,40167066,1192696589,2066073470,-329758024,-1546086506,-816996862]

短いプログラムだったが、パターンマッチをスクラッチから作る方法、文字列の置換、リストモナドの使い方、IOモナドのループを純粋関数で組む方法、乱数の使い方など盛りだくさんのテクニックが織り込まれていた。

これを見ても Haskell がとっつきやすさの割に奥が見えないプログラム言語なのが分かる。テクニックを知っていれば非常に短い記述で多くの事が最小限のエラーで実行できるが、知らないと、他の人のコードを読みこなすことすらできない。それは、Haskell に限ったことではないのだろうが、Haskell を勉強しているととくにそれを感じる。

また、上のようなプログラムを読むと、欧米では既に Haskell がかなり普及した言語なのではないかという危機感を感じる。並列コンピューティングの時代に突入しつつある今 Haskell をマスターすることは個性の強いプログラム言語を趣味で使うという以上の意味があるのではないだろうか。
by tnomura9 | 2012-07-21 19:58 | Haskell | Comments(0)
<< ZOKUDAM Eliza (5) apply... >>