函数式编程不会摆脱状态。它只是使它明确!确实,像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'传递给多个计算很容易...) 。这是monad进入的地方:它们不添加以前无法完成的任何事情,但是它们使您可以隐式传递状态变量,并且接口保证它以单线程方式发生。
编辑:我将尝试给出我现在所做的更多推理:首先,我对深度优先搜索进行了编码,而不仅仅是对可达性的测试。实现看起来几乎相同,但是调试看起来更好一些。
用有状态语言,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”,而不是强迫您手动完成。这不仅可以消除代码中的混乱情况,而且还可以防止您出错,例如派生状态(将相同的“已访问”设置传递给两个后续调用,而不是链接状态)。