関数型プログラミングは状態を取り除きません。明示的にするだけです!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))
そして今度は、「訪問済み」変数を取り除くという難しい部分があります。基本的なトリックは、状態を必要とする関数に追加のパラメーターとして状態を渡し、それらの関数が状態を変更したい場合に追加の戻り値として状態の新しいバージョンを返すようにする規則を使用することです。
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」を渡すことです。これはコードから混乱を取り除くだけでなく、状態の分岐(状態を連鎖する代わりに2つの後続の呼び出しに同じ "visited"セットを渡す)などのミスを防ぐこともできます。