함수형 프로그래밍 및 상태 저장 알고리즘


12

Haskell 과 함께 기능 프로그래밍을 배우고 있습니다 . 그 동안 나는 오토마타 이론을 공부하고 있고 두 사람이 잘 어울리는 것처럼 오토마타와 함께 연주 할 작은 도서관을 쓰고 있습니다.

여기 질문을 한 문제가 있습니다. 상태의 도달 가능성을 평가하는 방법을 연구하는 동안 간단한 재귀 알고리즘이 상당히 비효율적이라는 아이디어를 얻었습니다. 일부 경로는 일부 상태를 공유하고 결국 두 번 이상 평가할 수 있기 때문입니다.

예를 들어, 여기서, 평가 도달g 에서 , I는 제외 할 것이다 f를 통해 경로를 확인하는 동안 모두 DC를 :

오토 마톤을 나타내는 그래프

그래서 내 생각은 많은 경로에서 병렬로 작동하고 제외 된 상태의 공유 레코드를 업데이트하는 알고리즘이 좋을 수도 있지만 너무 나쁘다는 것입니다.

간단한 재귀 사례에서 상태를 인수로 전달할 수 있으며 루프를 피하기 위해 통과 한 상태 목록을 전달하기 때문에 여기서해야합니다. 그러나 내 canReach함수 의 부울 결과와 함께 튜플로 반환하는 것과 같이 목록을 뒤로 전달하는 방법이 있습니까? (이것은 조금 강요된 느낌이 들지만)

필자의 사례의 타당성 외에도 이러한 종류의 문제를 해결하는 데 사용할 수있는 다른 기술은 무엇입니까? 와 무슨와 같은 솔루션이있을 가지고 이러한 일반적인 충분히 있어야합니다 같은 느낌 fold*이나 map.

지금까지 learnyouahaskell.com을 읽으면 아무것도 찾지 못했지만 아직 모나드를 건드리지 않았다고 생각하십시오.

( 관심이 있다면 codereview에 코드를 게시 했습니다. )


3
우선, 당신이 작업하려고했던 코드를보고 싶습니다. 그것이 없다면, 나의 최선의 조언은 Haskell의 게으름이 종종 두 번 이상 계산하지 않기 위해 악용 될 수 있다는 것입니다. 소위 "매듭 묶기"와 게으른 값 재귀를 살펴보십시오.하지만 문제는 간단 할 것입니다.
Ptharien 's Flame 23시 08 분

1
@ Ptharien'sFlame 관심을 가져 주셔서 감사합니다! 여기 코드 가 있으며 전체 프로젝트에 대한 링크도 있습니다. 나는 이미 내가 진보 된 기술 :로보기에 좋은 것은 아닙니다, 그래서 그래, 지금까지 한 일을 혼동하고있어
bigstones

1
상태 오토마타는 함수형 프로그래밍의 대립과 거의 같습니다. 함수형 프로그래밍은 내부 상태없이 문제를 해결하는 것과 관련이 있으며 상태 자동 제어는 자체 상태를 관리하는 것입니다.
Philipp

@Philipp 동의하지 않습니다. 오토 마톤 또는 상태 머신은 때때로 문제를 나타내는 가장 자연스럽고 정확한 방법이며 기능적 오토마타는 잘 연구됩니다.
Ptharien 's Flame

5
@Philipp : 함수형 프로그래밍은 상태를 명시적인 것으로 만드는 것이지 금지하는 것이 아닙니다. 실제로 꼬리 재귀는 고토로 가득 찬 상태 머신을 구현하는 데 정말 유용한 도구입니다.
hugomg 2016 년

답변:


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...

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


나는 당신의 모범을 이해하는 데 어려움을 겪기 때문에 고통을 덜 느끼고 가독성을 높이는 방법이 있어야한다는 것을 알고있었습니다. 모나드를 찾거나 더 나은 코드를 이해하기 위해 연습해야합니까?
bigstones

@ bigstones : 모나드를 다루기 전에 코드가 어떻게 작동하는지 이해해야한다고 생각합니다. 기본적으로 내가했던 것과 똑같은 일을하지만 추가 추상화 계층을 사용하여 혼란스럽게합니다. 어쨌든, 나는 좀 더 명확하게하기 위해 약간의 설명을 추가했다
hugomg

1
"기능 프로그래밍은 상태를 제거하지 않습니다. 명시 적으로 만 만듭니다!": 이것은 정말 명확합니다!
Giorgio

"[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

여기서 neighbors상태는 즉시 상태에 연결된 상태를 반환합니다. 일련의 경로를 반환합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.