関数型プログラミングとステートフルアルゴリズム


12

Haskellで関数型プログラミングを学んでいます。それまでの間、私はオートマタ理論を研究しており、この2つがうまく調和しているように思えるので、オートマタで遊ぶための小さなライブラリを書いています。

これが私に質問をさせた問題です。状態の到達可能性を評価する方法を研究しているときに、いくつかのパスがいくつかの状態を共有する可能性があり、それらを複数回評価する可能性があるため、単純な再帰アルゴリズムは非常に非効率であるという考えを得ました。

たとえば、ここで、aからgの到達可能性を評価する場合、dcを通るパスをチェックする際にfの両方を除外する必要があります

オートマトンを表す有向グラフ

だから私の考えは、多くのパスで並行して動作し、除外された状態の共有レコードを更新するアルゴリズムは素晴らしいかもしれないということですが、それは私にとってはやり過ぎです。

いくつかの単純な再帰の場合、引数として状態を渡すことができることがわかりました。ループを回避するために通過した状態のリストを転送するため、ここで行う必要があります。しかし、リストを逆方向に渡す方法はありcanReachますか?たとえば、関数のブール結果と一緒にタプルで返すのですか?(これは少し強制的に感じますが)

例題の妥当性に加えて、この種の問題を解決するために利用できる他のテクニックは何ですか?これらはと何が起こるかのようなソリューションがあるように持っていることを一般的に十分でなければならないような気がしますfold*map

これまでのところ、learnyouahaskell.comを読んでも見つかりませんでしたが、まだモナドに触れていないことを考慮してください。

興味があれば、コードレビューにコードを投稿しました


3
私は、あなたが取り組んでいるコードを見てみたいと思っています。それがない場合、私の最善のアドバイスは、Haskellの怠lazさは、物事を複数回計算しないように悪用されることが多いということです。いわゆる「結び目」と怠valueな値の再帰を調べてください。ただし、問題は単純なので、無限の値や類似のものを利用するより高度な手法はやり過ぎであり、おそらく今すぐ混乱するでしょう。
プサリアンの炎

1
@ Ptharien'sFlameご関心をお寄せいただきありがとうございます!ここにコードがあり、プロジェクト全体へのリンクもあります。私はこれまでにやったことと既に混同しているので、はい、高度な技術を調べない方が良いです:)
bigstones 2013年

1
状態オートマトンは、ほとんど関数型プログラミングのアンチテーゼです。関数型プログラミングは内部状態なしで問題を解決することです。一方、状態オートマトンはそれ自体の状態を管理することです。
フィリップ

@フィリップ私は同意しません。オートマトンまたはステートマシンは、問題を表す最も自然で正確な方法である場合があり、機能オートマトンはよく研究されています。
プサリアンの炎

5
@Philipp:関数型プログラミングは、状態を明示することであり、禁止することではありません。実際、末尾再帰は、gotoでいっぱいのステートマシンを実装するための本当に素晴らしいツールです。
hugomg

回答:


16

関数型プログラミングは状態を取り除きません。明示的にするだけです!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"セットを渡す)などのミスを防ぐこともできます。


私はあなたの例を理解するのに苦労しているので、痛みを軽減し、おそらくより読みやすくする方法がなければならないと知っていました。モナドに行くべきですか、それともあなたのようなコードを理解するためにもっと練習すべきですか?
ビッグストーン

@bigstones:モナドに取り組む前に私のコードがどのように機能するかを理解しようとするべきだと思います-基本的に私がやったのと同じことをしますが、抽象化のレイヤーを追加して混乱させます。とにかく、少しわかりやすくするために追加の説明を追加しました
-hugomg

1
「関数型プログラミングは状態を取り除きません。明示的にするだけです!」:これは本当に明確です!
ジョルジオ14年

「[Monads]​​で暗黙的に状態変数を渡すことができ、インターフェイスはそれがシングルスレッド方式で発生することを保証します」<-これはモナドの説明的な記述です。この質問の文脈外では、「状態変数」を「クロージャ」に置き換えることができます
人造人間

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

Where neighborsは、状態にすぐに接続された状態を返します。これは一連のパスを返します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.