하스켈과 꼬리 재귀가 함께 얻을뿐만 아니라 다른 기능 언어와 꼬리 재귀하지 않습니다. 꼬리 재귀 (tail recursion)로 어떤 일이 벌어지고 있는지 알아보기 위해 몇 가지 간단한 코드를 수동으로 축소 해 보겠습니다. 여기에 map (1+)
의 꼬리 재귀 구현이 있습니다.
[] ++ ys = ys
(x:xs) ++ ys = x : (xs ++ ys)
은 이제
go [1,2,3,4,5] []
을 줄일 수 있습니다 :
go [] r = r
go (x:xs) r = go xs (r ++ [1+x])
또한 우리는 마음에 (++)
의 정의를 유지해야합니다. [x,y,z]
은 x:(y:(z:[]))
또는 짧은 x:y:z:[]
의 표기라는 점에 유의하십시오.
go [1,2,3,4,5] []
go [2,3,4,5] ([] ++ [2]) -- 2 here is actually the thunk 1+1, but
-- for compactness I am reducing earlier
go [3,4,5] (([] ++ [2]) ++ [3])
go [4,5] ((([] ++ [2]) ++ [3]) ++ [4])
go [5] (((([] ++ [2]) ++ [3]) ++ [4]) ++ [5])
go [] ((((([] ++ [2]) ++ [3]) ++ [4]) ++ [5]) ++ [6])
(((([] ++ [2]) ++ [3]) ++ [4]) ++ [5]) ++ [6]
((([2] ++ [3]) ++ [4]) ++ [5]) ++ [6]
(((2:([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
((2:(([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
(2:((([] ++ [3]) ++ [4]) ++ [5]) ++ [6]
2:(((([] ++ [3]) ++ [4]) ++ [5]) ++ [6]) -- first observable output
2:((([3] ++ [4]) ++ [5]) ++ [6])
2:((3:([] ++ [4]) ++ [5]) ++ [6])
2:(3:(([] ++ [4]) ++ [5]) ++ [6])
2:3:((([] ++ [4]) ++ [5]) ++ [6]) -- second observable output
2:3:(([4] ++ [5]) ++ [6])
2:3:(4:([] ++ [5]) ++ [6])
2:3:4:(([] ++ [5]) ++ [6]) -- third observable output
2:3:4:([5] ++ [6])
2:3:4:5:([] ++ [6]) -- fourth observable output
2:3:4:5:6:[] -- final output
출력의 각 항목은 괄호의 중첩 시리즈에서 바깥쪽으로 그것의 방법을 작동해야하는 방법을 참조하십시오? 이렇게하면 모든 출력을 얻기 위해 입력 크기에서 2 차 시간이 걸립니다. 또한 처음 몇 가지 항목이 느리게 생성되는 동작을 볼 수 있으며 목록의 끝에 도달하면 빠르 고 빠릅니다. 이 감소는 그것을 설명합니다.
여기서 주요 성능 문제는 목록의 끝에 새 요소를 추가하는 것이므로 추가 할 목록의 크기에 비례하여 시간이 걸립니다. 더 좋은 방법은 앞쪽에 죄수를 두는 것인데, 죄수 잔업 시간입니다. 이렇게하면 출력이 반전되므로 결과를 반대로해야합니다.
go [] r = reverse r
go (x:xs) r = go xs ((1+x):r)
reverse xs = rev xs [] -- roughly from the report prelude
rev [] r = r
rev (x:xs) r = rev xs (x:r)
그리고,의이 감소하자
go [1,2,3,4,5] []
go [2,3,4,5] [2]
go [3,4,5] [3,2]
go [4,5] [4,3,2]
go [5] [5,4,3,2]
go [] [6,5,4,3,2]
reverse [6,5,4,3,2]
rev [6,5,4,3,2] []
rev [5,4,3,2] [6]
rev [4,3,2] [5,6]
rev [3,2] [4,5,6]
rev [2] [3,4,5,6]
rev [] [2,3,4,5,6]
[2,3,4,5,6] -- first and all observable output!
그래서이 명확 첫 번째 버전보다 작품입니다. 이것은 좋은 메모리 성능을 가지고 있기 때문에 Scheme 및 ML과 같은 엄격한 언어에서 사용되는 스타일입니다. 그러나 몇 가지 단점이 있습니다.
- 출력을 생성하려면 먼저 모든 입력을 사용해야합니다. 실제로 모든 계산은 결과가 생성되기 전에 수행됩니다.
- 무한한 목록이 주어지면 출력이 나오지 않습니다.
- 이것은 이며, 이는
O(n)
시간이 걸리고 우리가하는 일과 아무런 관련이 없습니다. (모든 요소에 하나를 추가하고 순서를 유지하는 것과 어떤 관계가 있습니까?)
하스켈과 같은 게으른 언어에서는 더 잘할 수 있습니다. 이상하고 아름답게, 우리가하는 일은 더 순진하게 그것을 쓰는 것입니다.
go [] = []
go (x:xs) = (1+x):go xs
및 감소 :
go [1,2,3,4,5]
2:(go [2,3,4,5]) -- first observable output
2:3:(go [3,4,5]) -- second observable output
2:3:4:(go [4,5]) -- third observable output
2:3:4:5:(go [6]) -- fourth observable output
2:3:4:5:6:(go []) -- fifth observable output
2:3:4:5:6:[] -- final output
그것은 더 적은 노력이 필요하고, 심지어는 목록의 나머지를보기 전에 출력을 산출 시작, 그래서 스트림 계산에 좋은 성능을 가지고에 작동 무한 입력. 그리고 구현은 당신이 바라는만큼 간단하고 명백합니다.
하스켈에서 꼬리 재귀가 작동하는 방식에 대해 직감을 줄 수 있기를 바랍니다. 예를 들어, 꼬리 재귀를 제거하고 우리의 최종 go
과 비슷한 순진한 스타일로 다시 작성하는 것이 좋습니다. 직감을 사용하여이 게시물에서 "가능한 한 빨리 입력의 큰 접두사" (x:xs
을 반환하면 xs
을 계산하기 위해 수행해야 할 작업이 더 많이 있더라도 즉시 즉 x
이 발생합니다 (즉, (비) 액션에서의 게으름).
하스켈에서 일정 시간, 일정 공간 온라인 알고리즘을 작성할 수 있습니다. 간단한 메커니즘으로 이러한 메커니즘을 설명하는 것이 훨씬 쉽습니다. 문제의 간단한 인스턴스를 만들 수 있습니까? 그 중 하나를 선택하거나 코드 검토로 이동하십시오. –
@tel 귀하의 제안에 감사드립니다. 나는 나의 모범을 단순화하려고 노력했다. 지금 어떻게 보이나요? –
위의 코드 (a)는 tail-recursive이며 모든 입력이 소비 된 후에 만 전달되는 누적기를 구축하고 (b) ++ 응용 프로그램의 왼쪽 중첩 탑을 계산합니다. slowdown ++는 첫 번째 인수의 길이에 선형적인 시간을 소요합니다. 가능한 한 빨리 입력의 큰 접두어로 코드를 전달하도록 만들고, ++의 오른쪽에 재귀 호출을 둡니다. – pigworker