Haskell은 lazy-evaluation을 사용하여 재귀를 구현하므로 필요한 경우 값을 제공하겠다는 약속으로 모든 것을 처리합니다 (이를 썽크라고 함). 썽 크는 진행하는 데 필요한만큼만 줄어 듭니다. 이것은 수학적으로 표현을 단순화하는 방법과 유사하므로 그렇게 생각하는 것이 도움이됩니다. 평가 순서가 코드에 의해 지정 되지 않는다는 사실로 인해 컴파일러는 예전의 꼬리 호출 제거보다 훨씬 더 똑똑한 최적화를 수행 할 수 있습니다. 최적화를 원한다면 컴파일 -O2
하세요!
facSlow 5
사례 연구로 평가하는 방법을 살펴 보겠습니다 .
facSlow 5
5 * facSlow 4
5 * (4 * facSlow 3)
5 * (4 * (3 * facSlow 2))
5 * (4 * (3 * (2 * facSlow 1)))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
따라서 걱정했던 것처럼 계산이 발생하기 전에 숫자가 쌓여 있지만 걱정 과 달리facSlow
종료를 기다리는 함수 호출 스택이 없습니다. 각 감소가 적용되고 사라지고 스택 프레임 이 그대로 유지됩니다. wake (즉, (*)
엄격 하기 때문에 두 번째 인수의 평가를 트리거합니다).
Haskell의 재귀 함수는 매우 재귀적인 방식으로 평가되지 않습니다! 주위에 걸려있는 유일한 호출 스택은 곱셈 자체입니다. (*)
가 엄격한 데이터 생성자로 간주되는 경우 이를 보호 된 재귀라고합니다 (일반적으로 비 엄격 데이터 생성자에서 이와 같이 언급되지만 추가 액세스에 의해 강제되는 경우 데이터 생성자가 데이터 생성자입니다).
이제 tail-recursive를 살펴 보겠습니다 fac 5
.
fac 5
fac' 5 1
fac' 4 {5*1}
fac' 3 {4*{5*1}}
fac' 2 {3*{4*{5*1}}}
fac' 1 {2*{3*{4*{5*1}}}}
{2*{3*{4*{5*1}}}}
(2*{3*{4*{5*1}}})
(2*(3*{4*{5*1}}))
(2*(3*(4*{5*1})))
(2*(3*(4*(5*1))))
(2*(3*(4*5)))
(2*(3*20))
(2*60)
120
따라서 꼬리 재귀 자체가 시간이나 공간을 절약하지 못한 방법을 알 수 있습니다. 전체적으로 더 많은 단계를 수행 할뿐만 아니라 facSlow 5
중첩 된 썽크 (로 표시됨)를 구축합니다 {...}
. 추가 공간이 필요합니다. 이는 향후 계산, 수행 할 중첩 곱셈을 설명합니다.
이 썽크이어서 이송하여 풀린다 를 스택 계산을 재현 하단. 두 버전 모두 매우 긴 계산으로 스택 오버플로가 발생할 위험도 있습니다.
이를 수동으로 최적화하려면 엄격하게 설정하기 만하면됩니다. 엄격한 애플리케이션 연산자 $!
를 사용하여
facSlim :: (Integral a) => a -> a
facSlim x = facS' x 1 where
facS' 1 y = y
facS' x y = facS' (x-1) $! (x*y)
이것은 facS'
두 번째 주장에서 엄격해야합니다. ( facS'
적용 할 정의를 결정하기 위해 평가해야하므로 첫 번째 인수는 이미 엄격 합니다.)
때로는 엄격함이 큰 도움이 될 수 있으며, 게으름이 더 효율적이기 때문에 때로는 큰 실수입니다. 여기에 좋은 생각이 있습니다.
facSlim 5
facS' 5 1
facS' 4 5
facS' 3 20
facS' 2 60
facS' 1 120
120
당신이 달성하고 싶었던 것이 무엇인지 나는 생각한다.
요약
- 코드를 최적화하려면 첫 번째 단계는 다음으로 컴파일하는 것입니다.
-O2
- 꼬리 재귀는 썽크 축적이 없을 때만 유용하며, 엄격함을 추가하면 일반적으로 적절한 경우이를 방지하는 데 도움이됩니다. 이것은 나중에 필요한 결과를 한꺼번에 만들 때 발생합니다.
- 때때로 꼬리 재귀는 나쁜 계획이고 보호 된 재귀가 더 적합합니다. 참조 이 질문 에 대해
foldr
그리고 foldl
예를 들어, 서로에 대해 그들을 테스트합니다.
다음 두 가지를 시도하십시오.
length $ foldl1 (++) $ replicate 1000
"The size of intermediate expressions is more important than tail recursion."
length $ foldr1 (++) $ replicate 1000
"The number of reductions performed is more important than tail recursion!!!"
foldl1
꼬리 재귀이지만 foldr1
보호 된 재귀를 수행하여 추가 처리 / 액세스를 위해 첫 번째 항목이 즉시 표시되도록합니다. (한 번에 왼쪽의 첫 번째 "괄호" (...((s+s)+s)+...)+s
는 입력 목록을 완전히 끝까지 강제하고 전체 결과가 필요한 것보다 훨씬 빨리 미래 계산의 큰 덩어리를 구축합니다. 두 번째 괄호는 점차 오른쪽으로 이동 s+(s+(...+(s+s)...))
하여 입력을 모든 것이 최적화를 통해 일정한 공간에서 작동 할 수 있습니다).
사용중인 하드웨어에 따라 0의 수를 조정해야 할 수도 있습니다.