무한 목록이있는 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 : 단지 사이 foldl및 foldr과 엄격함 및 최적화, 제 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.