함수형 프로그래밍은 상태를 제거하지 않습니다. 그것은 단지 그것을 명시 적으로 만듭니다! 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...
이 패턴을 df에 적용하려면 "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"를 직접 전달하는 대신 암시 적으로 전달하는 것입니다. 이렇게하면 코드에서 혼란이 제거 될뿐만 아니라 상태를 연결하는 대신 동일한 "방문 된"집합을 두 개의 후속 호출로 전달하는 것과 같은 실수를 방지 할 수 있습니다.