Stateモナド
大域変数を使ったLispの練習問題をHaskellで挑戦していて、状態を全部、引数にして戻り値にするしかないなー、大変やと思ったら、それをしてくるモナドがありました。
「人工知能のためのLISP入門」p.126
-- -- 「人工知能のためのLISP入門」 by Peter Coxhead -- 6.1 探索技術 {- 次のようなパズルを考えてみよう。ある細長い板がいくつかの「マス目(SQUARE)」に 区切られているとしよう。マス目の数は正確には決っていないが、丁度、チェス盤の 1列のようなものである。いくつかの「駒」が左端に並んでいて、それぞれ異なった 文字が記されている。下記の規則に従ってこれらの駒を動かし、指定された文字の並びを 細長い板の右端に作ることがこのパズルのゴールである。 (1) 一度に1つの駒しか動かせない。 (2) 動かし方、押し移動と飛び越し移動である。 (3) 押し移動とは、すぐ右隣の空いているマス目に駒を動かすことである。 (4) 飛び越し移動は、1つ飛び越しを可能な限り多く行う。1つ飛び越しとは、1つの駒を 飛び越してその右側の空いているマス目に、駒を動かすことである。 -} -- 設計仕様 -- 文字列を使って駒の状態を表す。アルファベットは駒、ハイフンは空いているマス目を示す。 -- 縦型探索 -- 同じ状態は2度探索しない。 import Control.Monad.State.Strict import qualified Data.Set as Set import List newtype PuzzleState = PS String deriving (Eq, Ord, Show) main = print $ solve (PS "ART---") (PS "---TAR") class (Ord a) => Situation a where children :: a -> [a] -- 可能な次の状態すべてを返す。 distance :: a -> a -> Int -- 2つの状態の「距離」を返す。 type Path a = [a] solve :: Situation a => a -> a -> ([a], Set.Set a) -- startからgoalへ至る遷移パスがあれば、それと、探索の際に通過した状態を出力する。 -- searchで組み立てた演算戦略を実行する。 solve start goal = runState (search [[start]] goal) Set.empty search :: Situation a => [[a]] -> a -> State (Set.Set a) [a] -- sortqの優先順位で縦型探索する。 -- 探索した状態を持って探索するので、Stateモナド(を使った演算戦略)を返す。 search [] _ = return [] search (path@(s:_):paths) goal | s == goal = return (reverse path) | otherwise = do x <- expand path search (sortq (x ++ paths) goal) goal expand :: Situation a => [a] -> State (Set.Set a) [[a]] -- pathの先頭に可能な次の状態をそれぞれconsした配列を返す。 expand [] = return [] expand path@(s:_) = do x <- new (children s) return (map (\s -> s:path) x) new :: Situation a => [a] -> State (Set.Set a) [a] -- 引数の状態のうち、新たなものを返す。 new [] = return [] new (s:ss) = do b <- isGenerated s rest <- new ss return (if b then rest else s:rest) where isGenerated :: Situation a => a -> State (Set.Set a) Bool isGenerated state = do generated <- get put (Set.insert state generated) return (Set.member state generated) sortq :: Situation a => [Path a] -> a -> [Path a] -- 評価関数に従って探索パス候補を並び替える。 sortq paths goal = sortBy ordering paths where -- 型宣言がうまくいかないのでとりあえず省略 ordering path1 path2 = compare (efv path1 goal) (efv path2 goal) efv path@(p:_) goal = length path + distance p goal -- efvは評価関数 instance Situation PuzzleState where children (PS s) = map PS (children1 [] s) where children1 :: String -> String -> [String] children1 _ [] = [] children1 squaresPassed squaresLeft@(c:cs) = let move = moveFirstSquare squaresLeft in if move /= [] then (reverse squaresPassed ++ move):children1 (c:squaresPassed) cs else children1 (c:squaresPassed) cs moveFirstSquare :: String -> String moveFirstSquare [] = [] moveFirstSquare ('-':_) = [] moveFirstSquare (c:'-':cs) = '-':c:cs moveFirstSquare state@(_:_:'-':_) = jumpFirstSquare state moveFirstSquare _ = [] jumpFirstSquare :: String -> String jumpFirstSquare [] = [] jumpFirstSquare state@('-':_) = state jumpFirstSquare state@(_:'-':_) = state jumpFirstSquare (c0:c1:'-':cs) = '-':c1:(jumpFirstSquare (c0:cs)) jumpFirstSquare state = state distance (PS s1) (PS s2) = distance_ s1 s2 where distance_ [] _ = 0 distance_ (c:cs) goal@(_:gs) | c == '-' = distance_ cs gs | otherwise = countMoves c goal + distance_ cs gs where countMoves :: Char -> String -> Int countMoves _ [] = maxBound :: Int countMoves piece (g:gs) | piece == g = 0 | otherwise = 1 + countMoves piece gs
状態の代わりを変数の代わりに引数にして関数を数珠繋ぎにして状態の初期値の関数にした後(いわゆる「戦略」)、評価する。こりゃデバッグや検証が大変なんじゃなかろうか。
いやでも状態に依存する部分とそうでない部分が分かれていくところが面白いけど、末端でステートを入れたくなったとき、状態依存を持つようになる上位の関数すべて書き直さないといけない…なるほどこれが純粋関数型か。はたしてこれだけのコストを払う価値があるのか。
でも、なんでもないことをするのに知恵を絞らないといけない、というのがいいですね。凡人は湯水のようにアイデアが沸くわけじゃないから、何か思いつくまで、頭を刺激する別の何かが必要。