무한 목록이있는 foldl 대 foldr 동작


124

이 질문 의 myAny 함수에 대한 코드 는 foldr를 사용합니다. 술어가 충족되면 무한 목록 처리를 중지합니다.

foldl을 사용하여 다시 작성했습니다.

myAny :: (a -> Bool) -> [a] -> Bool
myAny p list = foldl step False list
   where
      step acc item = p item || acc

(단계 함수에 대한 인수가 올바르게 반전되었습니다.)

그러나 더 이상 무한 목록 처리를 중지하지 않습니다.

Apocalisp의 답변 에서와 같이 함수의 실행을 추적하려고 시도했습니다 .

myAny even [1..]
foldl step False [1..]
step (foldl step False [2..]) 1
even 1 || (foldl step False [2..])
False  || (foldl step False [2..])
foldl step False [2..]
step (foldl step False [3..]) 2
even 2 || (foldl step False [3..])
True   || (foldl step False [3..])
True

그러나 이것은 함수가 작동하는 방식이 아닙니다. 이것이 어떻게 잘못 되었습니까?

답변:


231

어떻게 fold다른가가 혼란의 빈번한 원인 인 것처럼 보이므로 다음은보다 일반적인 개요입니다.

[x1, x2, x3, x4 ... xn ]일부 함수 f및 seed를 사용 하여 n 값 목록을 접는 것을 고려하십시오 z.

foldl is :

  • 왼쪽 연관 :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail recursive : 목록을 반복하여 나중에 값을 생성합니다.
  • Lazy : 결과가 필요할 때까지 아무것도 평가되지 않습니다.
  • 뒤로 : foldl (flip (:)) []목록을 반대로합니다.

foldr is :

  • 오른쪽 연관 :f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
  • 인수로 재귀 : 각 반복 f은 다음 값과 나머지 목록을 접은 결과에 적용됩니다 .
  • Lazy : 결과가 필요할 때까지 아무것도 평가되지 않습니다.
  • Forwards : foldr (:) []변경되지 않은 목록을 반환합니다.

약간 미묘한 점은 여기에있다 가끔 여행 사람들까지 그 : 때문에 foldl입니다 뒤쪽 의 각 응용 프로그램 f받는 추가 외부 결과; lazy 이므로 결과가 필요할 때까지 아무것도 평가되지 않습니다. 즉, 결과의 일부를 계산하기 위해 Haskell은 먼저 중첩 된 함수 응용 프로그램의 표현식을 구성하는 전체 목록을 반복 한 다음 가장 바깥 쪽 함수를 평가하고 필요에 따라 인수를 평가합니다. f항상 첫 번째 인수를 사용하는 경우 Haskell이 가장 안쪽의 용어까지 계속 반복 한 다음의 각 응용 프로그램을 역으로 계산해야 함을 의미 f합니다.

이것은 분명히 대부분의 기능적인 프로그래머가 알고 사랑하는 효율적인 꼬리 재귀와는 거리가 멀다!

사실 foldl기술적으로는 꼬리 재귀 적이 지만 , 모든 결과를 평가하기 전에 전체 결과 표현식이 작성되기 때문에 foldl스택 오버플로가 발생할 수 있습니다!

반면에 foldr. 또한 게으르지 만 앞으로 실행되기 때문에의 각 응용 프로그램 이 결과 내부f추가됩니다 . 따라서 결과를 계산하기 위해 Haskell은 단일 함수 응용 프로그램을 구성하며 두 번째 인수는 나머지 접힌 목록입니다. 경우 데이터 생성자, 인스턴스 - - 두번째 인수 지연되는 결과가 될 것이다 증분 지연 계산할 접어서 각각의 단계, 때만 평가된다 필요로하는 결과의 일부.f

그래서 우리는 왜 foldr가끔 무한 목록에서 작동 하는지 알 수 있습니다 foldl. 전자는 무한 목록을 다른 지연 무한 데이터 구조로 느리게 변환 할 수있는 반면 후자는 결과의 일부를 생성하기 위해 전체 목록을 검사해야합니다. 반면에 foldr,와 같이 즉시 두 인수를 모두 필요로하는 함수를 사용하면 (+), 작동 (또는 작동하지 않음)과 매우 유사하게 foldl평가하기 전에 거대한 표현식을 작성합니다.

따라서 주목해야 할 두 가지 중요한 사항은 다음과 같습니다.

  • foldr 지연 재귀 데이터 구조를 다른 구조로 변환 할 수 있습니다.
  • 그렇지 않으면 지연 접기가 크거나 무한한 목록에서 스택 오버플로와 함께 충돌합니다.

당신은 그것이 foldr할 수있는 모든 것을 foldl할 수있는 것처럼 들리는 것을 알았을 것입니다 . 사실입니다! 사실, foldl은 거의 쓸모가 없습니다!

하지만 무한이 아닌 큰 목록을 접어서 지연되지 않은 결과를 생성하려면 어떻게해야할까요? 이를 위해 표준 라이브러리가 제공 하는 strict fold를 원합니다 .

foldl' is :

  • 왼쪽 연관 :f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
  • Tail recursive : 목록을 반복하여 나중에 값을 생성합니다.
  • 엄격함 : 각 기능 응용 프로그램은 그 과정에서 평가됩니다.
  • 뒤로 : foldl' (flip (:)) []목록을 반대로합니다.

때문에 foldl'입니다 엄격한 , 하스켈 것입니다 결과를 계산하는 평가 f 대신 왼쪽 인수를시키는의 각 단계에서 거대한, 평가되지 않은 표현을 축적. 이것은 우리가 원하는 일반적이고 효율적인 꼬리 재귀를 제공합니다! 다시 말해:

  • foldl' 큰 목록을 효율적으로 접을 수 있습니다.
  • foldl' 무한 목록에서 무한 루프 (스택 오버플로가 발생하지 않음)에서 중단됩니다.

Haskell 위키에도 이에 대해 논의하는 페이지 가 있습니다.


6
이유가 궁금 때문에 여기 온 foldr더 나은보다 foldl하스켈 반대의 사실이지만, 얼랑 (I 전에 배운 하스켈 ). 이후 얼랑 게으른하지 않고 기능이되지 않습니다 카레 그래서 foldl에서 얼랑 같은 동작합니다 foldl'위. 이것은 훌륭한 대답입니다! 잘하셨습니다. 감사합니다!
Siu Ching Pong -Asuka Kenji-

7
이것은 대부분 훌륭한 설명이지만 foldl"뒤로"및 foldr"앞으로"라는 설명이 문제가 있다고 생각합니다. 이것은 부분적으로 폴드가 뒤로 접히는 이유에 대한 설명에 flip적용 되기 때문 입니다 (:). 자연스러운 반응은 "물론 거꾸로입니다. flip목록 연결 을 핑했습니다!" 완전한 평가에서 첫 번째 목록 요소 (가장 안쪽)에 먼저 foldl적용 되기 때문에 "뒤로"라고하는 것도 이상합니다 f. 그것은의 foldr적용 ", 뒤로 실행"고 f먼저 마지막 요소.
Dave Abrahams 2014

1
@DaveAbrahams : 단지 사이 foldlfoldr과 엄격함 및 최적화, 제 1 수단 "바깥 쪽을"무시하지 "안쪽". 이것이 foldr무한 목록을 처리 할 수 ​​있고 처리 할 수 foldl없는 이유입니다 . 오른쪽 접기 f는 먼저 첫 번째 목록 요소에 적용 되고 꼬리 접기의 (평가되지 않은) 결과에 적용 되고 왼쪽 접기는 전체 목록을 통과하여 f.
CA McCann 2014

1
foldl '보다 foldl이 선호되는 경우가 있는지 궁금합니다. 하나가 있다고 생각하십니까?
kazuoua 2015

1
게으름이 필수적인 @kazuoua, 예 last xs = foldl (\a z-> z) undefined xs.
Will Ness

28
myAny even [1..]
foldl step False [1..]
foldl step (step False 1) [2..]
foldl step (step (step False 1) 2) [3..]
foldl step (step (step (step False 1) 2) 3) [4..]

기타

직관적 foldl으로 항상 "외부"또는 "왼쪽"에 있으므로 먼저 확장됩니다. 무한 광고.


10

Haskell의 문서 에서 foldl은 꼬리 재귀 적이며 무한 목록을 전달해도 끝나지 않을 것임을 알 수 있습니다 .


0

나는 Haskell을 모르지만 Scheme에서는 fold-right 항상 목록의 마지막 요소에서 먼저 '행동'합니다. 따라서 순환 목록 (무한 목록과 동일 함)에는 작동하지 않습니다.

fold-right꼬리 재귀로 작성할 수 있는지 확실하지 않지만 순환 목록의 경우 스택 오버플로가 발생해야합니다. fold-leftOTOH는 일반적으로 꼬리 재귀로 구현되며 일찍 종료하지 않으면 무한 루프에 갇히게됩니다.


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