재귀에서 기능적 언어가 더 낫습니까?


41

TL; DR : 기능적 언어는 비 기능적 언어보다 재귀를 더 잘 처리합니까?

저는 현재 Code Complete 2를 읽고 있습니다.이 책의 어느 시점에서 저자는 재귀에 대해 경고합니다. 그는 가능하면 피해야하며 재귀를 사용하는 함수는 일반적으로 루프를 사용하는 솔루션보다 덜 효과적이라고 말합니다. 예를 들어, 저자는 숫자의 계승을 계산하기 위해 재귀를 사용하여 Java 함수를 작성했습니다 (현재 나와 함께 책이 없기 때문에 정확히 같지 않을 수도 있음).

public int factorial(int x) {
    if (x <= 0)
        return 1;
    else
        return x * factorial(x - 1);
}

이것은 나쁜 해결책으로 제시됩니다. 그러나 기능적 언어에서는 재귀를 사용하는 것이 선호되는 방식입니다. 예를 들어, 재귀를 사용하는 Haskell의 계승 함수는 다음과 같습니다.

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

그리고 좋은 해결책으로 널리 받아 들여지고 있습니다. 내가 본 것처럼, Haskell은 재귀를 매우 자주 사용하며, 눈살을 찌푸리는 곳은 보지 못했습니다.

그래서 내 질문은 기본적으로 :

  • 기능적 언어는 비 기능적 언어보다 재귀를 더 잘 처리합니까?

편집 : 내가 사용한 예제가 내 질문을 설명하기에 최선이 아니라는 것을 알고 있습니다. 방금 Haskell (및 기능적 언어)이 비 기능적 언어보다 훨씬 자주 재귀를 사용한다는 것을 지적하고 싶었습니다.


10
적절한 기능 : 많은 기능적 언어가 테일 콜 최적화를 많이 사용하는 반면 절차 적 언어는 그렇게하지 않습니다. 이는 테일 콜 재귀가 해당 기능 언어에서 훨씬 저렴 하다는 것을 의미합니다 .
Joachim Sauer

7
사실, 당신이 준 Haskell 정의는 꽤 나쁩니다. factorial n = product [1..n]간결하고 효율적이며 스택을 오버플로하지 않습니다 n(메모 화가 필요한 경우 완전히 다른 옵션이 필요합니다). product일부의 관점에서 정의 fold되는 되는 재귀 적으로 정의하지만, 아주 조심. 재귀 대부분 허용되는 솔루션이지만 여전히 잘못 / 차선책은 쉽습니다.

1
@JoachimSauer-약간의 꾸밈음으로 귀하의 의견은 가치있는 답변을 드릴 것입니다.
Mark Booth

편집 결과 내 표류를 잡지 못했다고 표시됩니다. 당신이 준 정의는 재귀의 완벽한 예이며 , 기능적인 언어에서도 나쁘다 . 내 대안은 재귀 적이며 (라이브러리 함수에 있지만) 매우 효율적이며 재귀 방법 만 차이를 만듭니다. Haskell은 게으름이 일반적인 규칙을 위반한다는 점에서 이상한 경우입니다.

@delnan : 설명해 주셔서 감사합니다! 편집을 편집하겠습니다;)
marco-fiset

답변:


36

네,뿐만 아니라 그들이 때문에, 할 수있는 그들이 때문이 아니라 에 있습니다 .

여기서 핵심 개념은 순도입니다 . 순수한 함수는 부작용이없고 상태가없는 함수입니다. 함수형 프로그래밍 언어는 일반적으로 코드에 대한 추론 및 명백하지 않은 종속성 방지와 같은 여러 가지 이유로 순수성을 받아들입니다. Haskell과 같은 일부 언어는 순수한 코드 허용하기까지 합니다. 프로그램이 가질 수있는 부작용 (예 : I / O 수행)은 순수하지 않은 런타임으로 이동하여 언어 자체를 순수하게 유지합니다.

부작용이 없다는 것은 루프 카운터를 가질 수 없다는 것을 의미합니다 (루프 카운터가 변경 가능한 상태를 구성하고 그러한 상태를 수정하는 것이 부작용이기 때문에) 순수 기능 언어가 얻을 수있는 가장 반복적 인 것은 목록 을 반복하는 것입니다 ( 이 작업을 일반적으로 foreach또는 이라고 map합니다. 그러나 재귀는 순수한 함수형 프로그래밍과 자연스럽게 일치합니다. (읽기 전용) 함수 인수와 (쓰기 전용) 반환 값을 제외하고는 재귀에 상태가 필요하지 않습니다.

그러나 부작용이 없으면 재귀를보다 효율적으로 구현할 수 있으며 컴파일러가보다 적극적으로 최적화 할 수 있습니다. 필자는 그러한 컴파일러에 대해 깊이 연구하지는 않았지만 대부분의 함수형 프로그래밍 언어의 컴파일러는 꼬리 호출 최적화를 수행하며 일부는 특정 유형의 재귀 구문을 루프 뒤의 루프로 컴파일 할 수도 있습니다.


2
기록을 위해 꼬리 호출 제거는 순도에 의존하지 않습니다.
scarfridge

2
@ scarfridge : 물론 그렇지 않습니다. 그러나 순도가 주어지면 컴파일러가 꼬리 호출을 허용하도록 코드를 재정렬하는 것이 훨씬 쉽습니다.
tdammers

썽크를 만드는 동안 TCO를 수행 할 수 없기 때문에 GCC는 GHC보다 훨씬 나은 TCO 작업을 수행합니다.
dan_waterworth

18

재귀와 반복을 비교하고 있습니다. 없이 꼬리 호출 제거 불필요한 함수 호출이 없기 때문에, 반복은 참으로 더 효율적입니다. 또한 반복은 영원히 진행될 수 있지만 너무 많은 함수 호출로 인해 스택 공간이 부족할 수 있습니다.

그러나 반복을 수행하려면 카운터를 변경해야합니다. 즉 , 순전히 기능적인 설정에서는 금지되는 가변 변수 가 있어야 합니다. 따라서 함수형 언어는 반복 할 필요없이 작동하도록 특별히 설계되었으므로 간소화 된 함수 호출

그러나 그 중 어느 것도 코드 샘플이 그렇게 매끄러운 이유를 다루지 않습니다. 귀하의 예는 패턴 일치 인 다른 속성을 보여줍니다 . 그렇기 때문에 Haskell 샘플에는 명시적인 조건이 없습니다. 다시 말해, 코드를 작게 만드는 것은 간소화 된 재귀가 아닙니다. 패턴 일치입니다.


패턴 일치가 무엇인지 이미 알고 있으며 Haskell에서 내가 사용하는 언어에서 놓친 멋진 기능이라고 생각합니다!
marco-fiset

@marcof 내 요점은 재귀 대 반복에 대한 모든 이야기가 코드 샘플의 매끄러움을 다루지 않는다는 것입니다. 실제로 패턴 일치와 조건부에 관한 것입니다. 아마도 내 대답에 맨 위에 올려 놓았을 것입니다.
chrisaycock

그렇습니다, 나는 또한 그것을 이해했다 : P
marco-fiset

@ chrisaycock : 루프 본문에 사용 된 모든 변수가 재귀 호출의 인수 및 반환 값 인 꼬리 재귀로 반복을 볼 수 있습니까?
조르지오

@ Giorgio : 예, 함수가 같은 유형의 튜플을 가져 와서 반환하도록하십시오.
Ericson2314

5

기술적으로는 아니지만 실제로는 가능합니다.

문제에 대한 기능적 접근 방식을 취할 때 재귀가 훨씬 일반적입니다. 따라서, 기능적 접근 방식을 사용하도록 설계된 언어에는 종종 재귀를보다 쉽게 ​​/ 더 좋게 / 덜 문제있게 만드는 기능이 포함됩니다. 내 머리 꼭대기에는 세 가지 공통점이 있습니다.

  1. 테일 콜 최적화. 다른 포스터에서 지적했듯이 기능적 언어에는 종종 TCO가 필요합니다.

  2. 게으른 평가. Haskell (및 다른 언어)은 게으르게 평가됩니다. 이것은 필요할 때까지 메소드의 실제 '작업'을 지연시킵니다. 이것은 재귀적인 데이터 구조로 이어지고 재귀적인 방법으로 재귀적인 데이터 구조를 만드는 경향이 있습니다.

  3. 불변성. 함수형 프로그래밍 언어로 작업하는 대부분의 작업은 변경할 수 없습니다. 시간이 지남에 따라 객체의 상태에 신경 쓸 필요가 없기 때문에 재귀가 더 쉬워집니다. 예를 들어 아래에서 값을 변경할 수 없습니다. 또한 많은 언어가 순수한 기능 을 탐지하도록 설계되었습니다 . 순수한 함수에는 부작용이 없으므로 컴파일러는 함수가 실행되는 순서 및 기타 최적화에 대해 훨씬 더 많은 자유가 있습니다.

이 중 어떤 것도 실제로 다른 언어에 비해 기능적 언어에만 국한된 것이 없으므로 기능적 기능 때문에 단순히 더 나은 것은 아닙니다. 그러나 그것들은 기능 적이기 때문에, 디자인 결정은 기능적으로 프로그래밍 할 때 더 유용하고 (그리고 단점이 덜 문제가되기 때문에) 이러한 기능에 대한 경향이 있습니다.


1
재 : 1. 조기 반품은 테일 콜과 관련없습니다 . 꼬리 호출을 사용하여 일찍 반환 할 수 있으며 "늦은"반환에도 꼬리 호출이 가능 하며 꼬리 위치에 없는 재귀 호출을 사용하여 간단한 단일 식을 사용할 수 있습니다 (OP의 계승 정의 참조).

@delnan : 감사합니다; 그것은 일찍 일을 공부 한 이후 꽤 오래되었습니다.
Telastyn

1

Haskell 및 기타 기능 언어는 일반적으로 지연 평가를 사용합니다. 이 기능을 사용하면 끝없는 재귀 함수를 작성할 수 있습니다.

재귀가 끝나는 기본 사례를 정의하지 않고 재귀 함수를 작성하면 해당 함수와 스택 오버플로에 대한 무한 호출이 끝납니다.

Haskell은 재귀 함수 호출 최적화도 지원합니다. Java에서는 각 함수 호출이 누적되어 오버 헤드가 발생합니다.

그렇습니다. 기능적 언어는 다른 언어보다 재귀를 더 잘 처리합니다.


5
Haskell은 엄격하지 않은 몇 가지 언어 중 하나입니다. 전체 ML 계열 ( 게으름 을 추가 하는 일부 연구 분사 제외 ), 모든 인기있는 Lisp, Erlang 등은 모두 엄격합니다. 또한 두 번째 단락은 꺼져있는 것처럼 보입니다-첫 번째 단락에서 올바르게 말하면 게으름 무한 재귀를 허용합니다 (예 : Haskell 전주에는 매우 유용 forever a = a >> forever a합니다).

@ deinan : 내가 아는 한 SML / NJ는 게으른 평가를 제공하지만 SML에 추가됩니다. 또한 몇 가지 게으른 기능적 언어 중 하나 인 Miranda와 Clean을 말하고 싶었습니다.
조르지오

1

내가 아는 유일한 기술적 이유는 일부 기능적 언어 (및 내가 기억하는 경우 명령형 언어)가 재귀 적 호출마다 스택의 크기를 증가시키지 않는 재귀 적 메소드를 허용하는 꼬리 호출 최적화 기능을 가지고 있기 때문입니다 (예 : 재귀 호출) 스택의 현재 호출을 대체합니다).

참고이 최적화 작업을하지 않는 모든 재귀 호출 만 꼬리 호출 재귀 방법 (예. 재귀 호출시의 상태를 유지하지 않는 방법)


1
(1) 이러한 최적화는 매우 구체적인 경우에만 적용됩니다-OP의 예는 그렇지 않으며 다른 많은 간단한 함수는 꼬리 재귀가되기 위해 약간의주의가 필요합니다. (2) 리얼 꼬리 호출 최적화뿐만 아니라 재귀 함수를 최적화, 그것의 공간 오버 헤드를 제거 않습니다 어떤 즉시 반환 뒤에 전화를.

@delnan : (1) 그렇습니다. 이 답변의 내 '원래 초안'에서, 나는 (2) 네,하지만 질문의 맥락에서, 나는 그 언급에 관계없는 줄 알았는데 것을 :( 언급했다.
스티븐 에버스

예, (2)는 유용한 추가 사항입니다 (연속 통과 스타일에는 필수 불가결). 대답 할 필요는 없습니다.

1

가비지 콜렉션이 빠르지 만 스택이 빠름 을보고 싶을 것입니다 .C 프로그래머가 컴파일 된 C의 스택 프레임에 대해 "힙"으로 생각하는 것에 대한 논문입니다. 저는 저자가 Gcc와 함께 고민했다고 생각합니다. . 정답은 아니지만 재귀와 관련된 문제를 이해하는 데 도움이 될 수 있습니다.

하기 Alef 프로그래밍 언어 플랜 9과 함께 제공하는 데 사용하는 "가"문 (참조 섹션 6.6.4 한 이 참조 ). 일종의 명시적인 테일 콜 재귀 최적화입니다. "하지만 콜 스택을 사용합니다!" 재귀에 대한 논쟁은 잠재적으로 사라질 수 있습니다.


0

TL; DR : 그렇습니다 .
재귀는 함수형 프로그래밍의 핵심 도구이므로 이러한 호출을 최적화하기 위해 많은 작업이 수행되었습니다. 예를 들어, R5RS는 프로그래머가 스택 오버플로에 대해 걱정하지 않고 모든 구현이 바인딩되지 않은 테일 재귀 호출을 처리하도록 요구합니다 (사양에서!). 비교를 위해 기본적으로 C 컴파일러는 명백한 테일 콜 최적화 (링크 목록의 재귀 반전 시도)조차하지 않으며 일부 호출 후 프로그램이 종료됩니다 (하지만 컴파일러를 사용하면 최적화됩니다- O2).

물론 fib지수로 유명한 유명한 예제 와 같이 끔찍하게 작성된 프로그램 에서 컴파일러는 '마법'을 수행 할 수있는 옵션이 거의 없습니다. 따라서 최적화에서 컴파일러의 노력을 방해하지 않도록주의해야합니다.

편집 : fib 예제에서 나는 다음을 의미합니다.

(define (fib n)
 (if (< n 3) 1 
  (+ (fib (- n 1)) (fib (- n 2)))
 )
)

0

기능적 언어는 꼬리 재귀와 무한 재귀라는 두 가지 매우 특정한 재귀 유형에서 더 좋습니다. 그들은 당신의 factorial예 와 같이 다른 종류의 재귀에서 다른 언어와 마찬가지로 나쁩니다 .

두 패러다임에서 규칙적인 재귀와 잘 작동하는 알고리즘이 없다고 말하는 것은 아닙니다. 예를 들어, 깊이 우선 트리 검색과 같이 스택과 유사한 데이터 구조가 필요한 것은 재귀로 구현하는 것이 가장 간단합니다.

재귀는 함수형 프로그래밍에서 더 자주 발생하지만, 특히 초보자 나 초보자를위한 자습서에서는 함수 프로그래밍을 많이 사용합니다. 아마도 함수형 프로그래밍 초보자는 명령 프로그래밍에서 재귀를 사용했기 때문일 것입니다. 목록 이해, 고차 함수 및 컬렉션의 다른 연산과 같은 다른 기능적 프로그래밍 구성이 있습니다. 일반적으로 개념, 스타일, 간결성, 효율성 및 최적화 기능에 훨씬 적합합니다.

예를 들어, delnan의 제안은 factorial n = product [1..n]더 간결하고 읽기 쉬울뿐만 아니라 병렬화도 가능합니다. 를 사용하는 동일 fold또는 reduce언어가 가지고있는 일이없는 경우 product이미 내장되어 있습니다. 재귀는이 문제에 대한 최후의 솔루션입니다. 튜토리얼에서 재귀 적으로 해결 된 주된 이유는 모범 사례의 예가 아니라 더 나은 솔루션을 얻기 전에 뛰어 들기 때문입니다.

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