누군가 Haskell의 메모에 대한 개념을 설명 할 수 있습니까?


12

(코딩 문제가 아닌 개념적 역학에 관한 것이기 때문에 여기서 질문하고 있습니다)

나는 작은 규모의 피보나치 수를 사용하는 작은 프로그램을 연구하고 있었지만 특정 수를 초과하면 고통스럽게 느려졌으며 약간의 인터넷 검색이 Haskell의 기술을 우연히 발견했습니다 Memoization. 그들은 다음과 같이 작동하는 코드를 보여주었습니다.

-- Traditional implementation of fibonacci, hangs after about 30
slow_fib :: Int -> Integer
slow_fib 0 = 0
slow_fib 1 = 1
slow_fib n = slow_fib (n-2) + slow_fib (n-1)

-- Memorized variant is near instant even after 10000
memoized_fib :: Int -> Integer
memoized_fib = (map fib [0 ..] !!)
   where fib 0 = 0
         fib 1 = 1
         fib n = memoized_fib (n-2) + memoized_fib (n-1)

그래서 여러분들에게 내 질문은, 어떻게 또는 왜 이것이 작동합니까?

계산이 끝나기 전에 어떻게 든 대부분의 목록을 실행하기 때문입니까? 그러나 haskell이 게으 르면 실제로 따라야 할 계산이 없습니다 ... 어떻게 작동합니까?


1
당신은 무엇을 의미하는지 명확히 할 수 the calculation catches up있습니까? BTW, 메모이 제이션 하스켈 특정되지 않습니다 : en.wikipedia.org/wiki/Memoization
사이먼 Bergot

killan의 답변 아래 내 설명을 참조하십시오
Electric Coffee

2
당신의 질문을 사랑하십시오; 그냥 빨리 참고 :이 기술은 메모라고 내가 하지 메모, zation zation.
Racheet

답변:


11

실제 메모의 배후에있는 메커니즘을 설명하기 위해

memo_fib = (map fib [1..] !!)

평가되지 않은 계산 인 "썽크"목록을 생성합니다. 우리가 그것들을 건드리지 않는 한, 그것들은 열리지 않는 선물처럼 생각합니다.

이제 썽크를 평가 한 후에는 다시 평가하지 않습니다. 이것은 실제로 "정상적인"하스켈에서 유일한 형태의 돌연변이이며, 뭉크는 일단 구체적인 값이된다고 평가됩니다.

따라서 코드로 돌아가서 썽크 목록이 있으며이 트리 재귀를 계속 수행하지만 목록을 사용하여 재귀하고 목록의 요소가 평가되면 다시 계산되지 않습니다. 따라서, 순진한 fib 함수에서 트리 재귀를 피합니다.

접선 적으로 흥미로운 메모로서, 목록이 한 번만 평가되므로 일련의 피보나치 수를 계산하는 것보다 빠릅니다. 즉 memo_fib 10000, 두 번 계산 하면 두 번째 시간이 즉각적이어야합니다. Haskell은 함수에 대한 인수를 한 번만 평가했으며 람다 대신 부분 응용 프로그램을 사용하기 때문입니다.

TLDR : 목록에 계산을 저장하면 목록의 각 요소가 한 번 평가되므로 각 피보나치 수는 전체 프로그램에서 정확히 한 번만 계산됩니다.

심상:

 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_5]
 -- Evaluating THUNK_5
 [THUNK_1, THUNK_2, THUNK_3, THUNK_4, THUNK_3 + THUNK_4]
 [THUNK_1, THUNK_2, THUNK_1 + THUNK_2, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 1 + 1, THUNK_4, THUNK_3 + THUNK_4]
 [1, 1, 2, THUNK_4, 2 + THUNK4]
 [1, 1, 2, 1 + 2, 2 + THUNK_4]
 [1, 1, 2, 3, 2 + 3]
 [1, 1, 2, 3, 5]

THUNK_4하위 표현식이 이미 평가되었으므로 평가 방법 이 훨씬 빠릅니다.


목록의 값이 짧은 순서로 동작하는 방법에 대한 예를 제공 할 수 있습니까? 나는 그것이 작동하는 방식의 시각화에 추가 할 수 있다고 생각합니다 ... 그리고 memo_fib같은 값을 두 번 호출 하면 두 번째 시간이 즉각적이지만 사실 1을 더 높은 값으로 호출하면 사실입니다. 여전히 평가하는 데 시간이 오래 걸립니다 (30에서 31로 이동)
Electric Coffee

@ElectricCoffee 추가됨
Daniel Gratzer

@ElectricCoffee 아니, 이후하지 않습니다 memo_fib 29memo_fib 30이미 평가, 정확히 한이 두 번호 : 뭔가 evaled되면,이 evaled 상태로 유지를 추가하는 데 걸리는로 이동합니다.
Daniel Gratzer

1
그렇지 않으면 당신은 어떤 성능을 얻을하지 않습니다, 귀하의 재귀 목록을 가야 @ElectricCoffee
다니엘 Gratzer

2
@ElectricCoffee 예. 그러나 목록의 31 번째 요소는 과거 계산을 사용하지 않고 있습니다. 예를 기억하고 있지만 매우 쓸모없는 방식으로 반복됩니다. 아주, 아주 느리게
다니엘 Gratzer에게

1

메모의 요점은 동일한 기능을 두 번 계산하지 않는 것입니다.이 기능은 정확성에 영향을주지 않고 프로세스를 완전히 자동화 할 수 있기 때문에 순전히 기능적인 (즉, 부작용없이) 계산 속도를 높이는 데 매우 유용합니다. 이것은 순진하게 구현 될 때 트리 재귀 , 즉 지수 노력으로 fibo이어지는 같은 함수에 특히 필요합니다 . (이것이 피보나치 숫자가 실제로 재귀를 가르치는 데 매우 나쁜 예인 이유 중 하나입니다. 튜토리얼이나 서적에서 찾은 거의 모든 데모 구현은 큰 입력 값에 사용할 수 없습니다.)

당신은 실행의 흐름을 추적하는 경우, 값이 두 번째 경우에 그것을 볼 fib x때 항상 사용할 수 있습니다 fib x+1실행, 그리고 그동안 런타임 시스템은 단순히 메모리가 아닌 다른 재귀 호출을 통해 읽을 수 있습니다 첫 번째 솔루션은 더 작은 값에 대한 결과를 사용할 수 있기 전에 더 큰 솔루션을 계산하려고합니다. 이는 반복자 [0..n]가 왼쪽에서 오른쪽으로 평가되므로으로 시작하기 때문에 0첫 번째 예제의 재귀는로 시작하여 n질문을하기 때문 n-1입니다. 이것이 많은 불필요한 중복 함수 호출로 이어집니다.


아, 요점을 이해합니다. 코드에서 볼 수있는 것과 같이 작동 방식을 이해하지 못했습니다. memorized_fib 20예를 들어 쓸 때 실제로 쓰는 map fib [0..] !! 20것입니다. 전체 범위의 숫자가 최대 20입니까, 아니면 여기에 뭔가 빠졌습니까?
전기 커피

1
예,하지만 각 번호에 대해 한 번만. 순진한 구현은 fib 2너무 자주 계산 하여 머리를 돌리고 콜 트리 모피를 작은 값으로 적습니다 n==5. 당신이 저장하는 것을 본 후에는 다시 메모를 잊지 않을 것입니다.
Kilian Foth

@ElectricCoffee : 그렇습니다. 1에서 20까지의 fib를 계산할 것입니다. 그 전화에서 아무것도 얻지 못합니다. 이제 fib 21을 계산해보십시오. 1-21을 계산하는 대신 이미 1-20을 계산 했으므로 다시 계산할 필요가 없기 때문에 21을 계산할 수 있습니다.
Phoshi

나는에 대한 호출 트리를 기록하기 위해 노력하고있어 n = 5, 나는 현재 어디 지점에 도달했습니다 n == 3, 지금까지 좋은 때문에,하지만 어쩌면이 생각 그냥 내 필수적 마음은, 그러나하지 않습니다 그것을 위해 그런 단지 평균 n == 3, 당신은 그냥 얻을 map fib [0..]!!3? 그런 다음 어느 fib n프로그램 의 분기 로 이동 합니까 ... 사전 계산 된 데이터의 이점을 정확히 어디서 얻을 수 있습니까?
Electric Coffee

1
아냐 memoized_fib괜찮아 그건 slow_fib당신이 그것을 추적하면 그것이 당신을 울게 만들 것입니다.
Kilian Foth
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.