Haskellで関数型プログラミングを学んでいます。その間、私はオートマタ理論を研究していて、2つがうまく調和しているように見えるので、私はオートマトンで遊ぶ小さなライブラリを書いています。
これが私に質問をさせた問題です。状態の到達可能性を評価する方法を検討しているときに、単純な再帰アルゴリズムは非常に非効率的であるとの考えに達しました。パスによっては、一部の状態を共有し、最終的に複数回評価する可能性があるためです。
たとえば、ここではaからgのreachabilityを評価しているので、除外する必要がありますfdとcを介してパスをチェックする場合:
だから私の考えは、多くのパスで並行して動作し、除外された状態の共有レコードを更新するアルゴリズムは素晴らしいかもしれないということですが、それは私には多すぎます。
いくつかの単純な再帰のケースでは、引数として状態を渡すことができ、ループを回避するために通過した状態のリストを転送するため、これをここで実行する必要があることを確認しました。しかし、canReach
関数のブール結果と一緒にタプルでリストを返すように、そのリストを逆方向に渡す方法はありますか? (これは少し強制されたように感じますが)
私の事例の妥当性に加えて、この種の問題を解決するために利用できる他のテクニックは何ですか?これらは、fold*
またはmap
で何が起こるかなどの解決策が必要になるほど十分に一般的である必要があるように感じます。
これまでのところ、 learnyouahaskell.com を読んでいますが、何も見つかりませんでしたが、モナドにはまだ触れていません。
(興味があれば、 codereview にコードを投稿しました)
関数型プログラミングは状態を取り除きません。明示するだけです! mapのような関数がしばしば「共有」データ構造を「解明」することは事実ですが、到達可能性アルゴリズムを作成することだけが目的の場合は、すでに訪問したノードを追跡するだけの問題です。
import qualified Data.Set as S
data Node = Node Int [Node] deriving (Show)
-- Receives a root node, returns a list of the node keyss visited in a depth-first search
dfs :: Node -> [Int]
dfs x = fst (dfs' (x, S.empty))
-- This worker function keeps track of a set of already-visited nodes to ignore.
dfs' :: (Node, S.Set Int) -> ([Int], S.Set Int)
dfs' (node@(Node k ns), s )
| k `S.member` s = ([], s)
| otherwise =
let (childtrees, s') = loopChildren ns (S.insert k s) in
(k:(concat childtrees), s')
--This function could probably be implemented as just a fold but Im lazy today...
loopChildren :: [Node] -> S.Set Int -> ([[Int]], S.Set Int)
loopChildren [] s = ([], s)
loopChildren (n:ns) s =
let (xs, s') = dfs' (n, s) in
let (xss, s'') = loopChildren ns s' in
(xs:xss, s'')
na = Node 1 [nb, nc, nd]
nb = Node 2 [ne]
nc = Node 3 [ne, nf]
nd = Node 4 [nf]
ne = Node 5 [ng]
nf = Node 6 []
ng = Node 7 []
main = print $ dfs na -- [1,2,5,7,3,6,4]
今、私はこのすべての状態を手で追跡することはかなり面倒でエラーが発生しやすいことを告白しなければなりません(sの代わりにs 'を使用するのは簡単です' '、同じs'を複数の計算に渡すのは簡単です...) 。これがモナドの出番です。モナドはこれまでにできなかったことを追加しませんが、暗黙的に状態変数を渡すことができ、インターフェイスはシングルスレッドで発生することを保証します。
編集:私が今やったことをもっと推論しようとします:まず、到達可能性をテストするだけでなく、深さ優先検索をコーディングしました。実装はほとんど同じに見えますが、デバッグは少し良く見えます。
ステートフル言語では、DFSは次のようになります。
visited = set() #mutable state
visitlist = [] #mutable state
def dfs(node):
if isMember(node, visited):
//do nothing
else:
visited[node.key] = true
visitlist.append(node.key)
for child in node.children:
dfs(child)
次に、変更可能な状態を取り除く方法を見つける必要があります。まず最初に、dfsにvoidではなくそれを返すようにして、「visitlist」変数を削除します。
visited = set() #mutable state
def dfs(node):
if isMember(node, visited):
return []
else:
visited[node.key] = true
return [node.key] + concat(map(dfs, node.children))
そして、ここでトリッキーな部分が来ます:「visited」変数を取り除くことです。基本的なトリックは、それを必要とする関数に追加のパラメーターとして状態を渡し、それらの関数が状態を変更したい場合に追加の戻り値として状態の新しいバージョンを返すようにする規則を使用することです。
let increment_state s = s+1 in
let extract_state s = (s, 0) in
let s0 = 0 in
let s1 = increment_state s0 in
let s2 = increment_state s1 in
let (x, s3) = extract_state s2 in
-- and so on...
このパターンをdfsに適用するには、「visited」セットを追加パラメーターとして受け取り、「visited」の更新バージョンを追加の戻り値として返すように変更する必要があります。さらに、「visited」配列の「最新」バージョンを常に転送するようにコードを書き直す必要があります。
def dfs(node, visited1):
if isMember(node, visited1):
return ([], visited1) #return the old state because we dont want to change it
else:
curr_visited = insert(node.key, visited1) #immutable update, with a new variable for the new value
childtrees = []
for child in node.children:
(ct, curr_visited) = dfs(child, curr_visited)
child_trees.append(ct)
return ([node.key] + concat(childTrees), curr_visited)
Haskellバージョンは、私がここで行ったこととほぼ同じですが、変更可能な「curr_visited」変数と「childtrees」変数ではなく、内部再帰関数を使用します。
モナドに関しては、それらが基本的に達成することは、手動で行うように強制する代わりに、暗黙的に「curr_visited」を渡すことです。これにより、コードが煩雑になるのを防ぐだけでなく、状態の分岐(同じ「visited」セットを2つの後続の呼び出しに渡し、状態をチェーンするのではなく)などのミスを防ぐことができます。
これがmapConcat
に依存する簡単な答えです。
mapConcat :: (a -> [b]) -> [a] -> [b]
-- mapConcat is in the std libs, mapConcat = concat . map
type Path = []
isReachable :: a -> Auto a -> a -> [Path a]
isReachable to auto from | to == from = [[]]
isReachable to auto from | otherwise =
map (from:) . mapConcat (isReachable to auto) $ neighbors auto from
neighbors
は、状態に直接接続されている状態を返します。これは一連のパスを返します。