지연 평가가 유용한 이유는 무엇입니까?


119

왜 게으른 평가가 유용한 지 오랫동안 궁금해했습니다. 나는 아직 말이되는 방식으로 나에게 설명하는 사람이 없습니다. 대부분은 "나를 믿어 라"로 끝이납니다.

참고 : 나는 메모를 의미하지 않습니다.

답변:


96

대부분 더 효율적일 수 있기 때문에 값을 사용하지 않을 경우 계산할 필요가 없습니다. 예를 들어 함수에 세 개의 값을 전달할 수 있지만 조건식의 시퀀스에 따라 실제로 하위 집합 만 사용할 수 있습니다. C와 같은 언어에서는 어쨌든 세 가지 값이 모두 계산됩니다. 그러나 Haskell에서는 필요한 값만 계산됩니다.

또한 무한 목록과 같은 멋진 것들을 허용합니다. C와 같은 언어로는 무한 목록을 가질 수 없지만 Haskell에서는 문제가되지 않습니다. 무한 목록은 수학의 특정 영역에서 상당히 자주 사용되므로이를 조작하는 능력이 있으면 유용 할 수 있습니다.


6
파이썬은 반복자를 통해 무한 목록을 느리게 평가했습니다
Mark Cidade

4
생성기 및 생성기 표현식 (목록 이해와 유사한 방식으로 작동)을 사용하여 파이썬에서 무한 목록을 실제로 에뮬레이트 할 수 있습니다. python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
생성기는 Python에서 게으른 목록을 쉽게 만들지 만 다른 게으른 평가 기술과 데이터 구조는 눈에 띄게 덜 우아합니다.
Peter Burns

3
이 답변에 동의하지 않을 것 같습니다. 게으름은 효율성에 관한 것이라고 생각했지만 Haskell을 상당량 사용하고 Scala로 전환하여 경험을 비교하면 게으름이 자주 중요하지만 효율성 때문에 거의 중요하지 않다고 말해야했습니다. 나는 Edward Kmett가 진짜 이유에 맞았다 고 생각합니다.
오웬

3
나는 비슷하게 동의하지 않지만 엄격한 평가 때문에 C에서 무한 목록에 대한 명확한 개념이 없지만 썽크를 사용하고 함수를 전달하여 다른 언어 (실제로 대부분의 모든 게으른 언어의 실제 구현에서)에서 동일한 트릭을 쉽게 재생할 수 있습니다. 유사한 표현식에 의해 생성 된 무한 구조의 유한 접두사로 작업하기위한 포인터.
Kristopher Micinski 2012 년

71

지연 평가의 유용한 예는 다음을 사용하는 것입니다 quickSort.

quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)

이제 목록의 최소값을 찾으려면 다음을 정의 할 수 있습니다.

minimum ls = head (quickSort ls)

먼저 목록을 정렬 한 다음 목록의 첫 번째 요소를 가져옵니다. 그러나 게으른 평가 때문에 머리 만 계산됩니다. 예를 들어 목록의 최소값을 취하면[2, 1, 3,] quickSort는 먼저 2보다 작은 모든 요소를 ​​필터링합니다. 그런 다음 이미 충분한 QuickSort를 수행합니다 (단일 목록 [1] 반환). 지연 평가로 인해 나머지는 정렬되지 않으므로 계산 시간이 많이 절약됩니다.

이것은 물론 매우 간단한 예이지만 게으름은 매우 큰 프로그램에 대해 동일한 방식으로 작동합니다.

그러나이 모든 것에는 단점이 있습니다. 프로그램의 런타임 속도와 메모리 사용량을 예측하기가 더 어려워집니다. 이것은 게으른 프로그램이 느리거나 더 많은 메모리를 차지한다는 것을 의미하지는 않지만 알아두면 좋습니다.


19
보다 일반적으로 take k $ quicksort listO (n + k log k) 시간 만 걸립니다 n = length list. 지연되지 않은 비교 정렬을 사용하면 항상 O (n log n) 시간이 걸립니다.
ephemient

@ephemient 당신은 O (nk log k)를 의미하지 않습니까?
MaiaVictor 2013-06-11

1
@Viclib 아니, 내가 말한 것을 의미했습니다.
ephemient

나는 슬프게도, 그것을 얻을하지 않습니다 생각 다음 @ephemient
MaiaVictor

2
@Viclib n에서 상위 k 개 요소를 찾기위한 선택 알고리즘은 O (n + k log k)입니다. 게으른 언어로 퀵 정렬을 구현하고 처음 k 개 요소를 결정할 수있을 정도로만 평가하면 (이후 평가 중지) 비 지연 선택 알고리즘과 똑같은 비교가 수행됩니다.
ephemient

70

게으른 평가는 여러 가지에 유용합니다.

첫째, 기존의 모든 게으른 언어는 순수합니다. 게으른 언어에서는 부작용에 대해 추론하기가 매우 어렵 기 때문입니다.

순수 언어를 사용하면 방정식 추론을 사용하여 함수 정의에 대해 추론 할 수 있습니다.

foo x = x + 3

안타깝게도 지연이 아닌 설정에서는 지연 설정보다 더 많은 문이 반환되지 않으므로 ML과 같은 언어에서는 유용하지 않습니다. 그러나 게으른 언어에서는 평등에 대해 안전하게 추론 할 수 있습니다.

둘째, 하스켈과 같은 게으른 언어에서는 ML의 '값 제한'과 같은 많은 것들이 필요하지 않습니다. 이로 인해 구문이 크게 정리됩니다. 언어와 같은 ML은 var 또는 fun과 같은 키워드를 사용해야합니다. Haskell에서 이러한 것들은 하나의 개념으로 축소됩니다.

셋째, 게으름을 사용하면 조각으로 이해할 수있는 매우 기능적인 코드를 작성할 수 있습니다. Haskell에서는 다음과 같은 함수 본문을 작성하는 것이 일반적입니다.

foo x y = if condition1
          then some (complicated set of combinators) (involving bigscaryexpression)
          else if condition2
          then bigscaryexpression
          else Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

이렇게하면 함수 본문을 이해하면서 '하향식'으로 작업 할 수 있습니다. ML과 유사한 언어 let는 엄격하게 평가되는를 사용하도록 강요합니다 . 결과적으로 값이 비싸거나 부작용이있는 경우 항상 평가되는 것을 원하지 않기 때문에 let 절을 함수의 본문으로 '리프트'할 수 없습니다. Haskell은 해당 절의 내용이 필요한 경우에만 평가된다는 것을 알고 있으므로 where 절에 대한 세부 정보를 명시 적으로 '푸시'할 수 있습니다.

실제로 우리는 가드를 사용하는 경향이 있으며 다음을 위해 더 축소합니다.

foo x y 
  | condition1 = some (complicated set of combinators) (involving bigscaryexpression)
  | condition2 = bigscaryexpression
  | otherwise  = Nothing
  where some x y = ...
        bigscaryexpression = ...
        condition1 = ...
        condition2 = ...

넷째, 게으름은 때때로 특정 알고리즘의 훨씬 더 우아한 표현을 제공합니다. Haskell의 게으른 '빠른 정렬'은 한 줄짜리이며 처음 몇 개의 항목 만 보면 해당 항목 만 선택하는 비용에 비례하는 비용 만 지불한다는 이점이 있습니다. 이 작업을 엄격하게 방해하는 것은 없지만 동일한 점근 적 성능을 얻으려면 매번 알고리즘을 다시 코딩해야합니다.

다섯째, 게으름을 통해 언어로 새로운 제어 구조를 정의 할 수 있습니다. 엄격한 언어로 구문과 같이 새로운 'if .. then .. else ..'를 작성할 수 없습니다. 다음과 같은 함수를 정의하려고하면 :

if' True x y = x
if' False x y = y

엄격한 언어에서는 조건 값에 관계없이 두 분기가 모두 평가됩니다. 루프를 고려하면 더 나빠집니다. 모든 엄격한 솔루션에는 일종의 인용문 또는 명시 적 람다 구성을 제공하는 언어가 필요합니다.

마지막으로, 같은 맥락에서 모나드와 같은 유형 시스템의 부작용을 처리하는 가장 좋은 메커니즘 중 일부는 실제로 게으른 설정에서만 효과적으로 표현할 수 있습니다. 이것은 F #의 워크 플로의 복잡성을 Haskell Monads와 비교하여 확인할 수 있습니다. (엄격한 언어로 모나드를 정의 할 수 있지만, 안타깝게도 게으름과 워크 플로 부족으로 인해 모나드 법칙을 한두 번 실패하는 경우가 많습니다.


5
아주 좋습니다. 이것이 진정한 답입니다. 나는 Haskell을 상당히 많이 사용하고 그것이 전혀 이유가 아니라는 것을 알기 전까지는 효율성에 관한 것이라고 생각했습니다 (나중을 위해 계산 지연).
오웬

11
또한 게으른 언어가 순수해야한다는 것은 기술적으로 사실이 아니지만 (예로서 R), 불순한 게으른 언어가 매우 이상한 일을 할 수 있다는 것은 사실입니다 (예로서 R).
오웬

4
물론입니다. 엄격한 언어에서 재귀 let는 위험한 짐승입니다. R6RS 체계 #f에서는 매듭을 묶는 것이 엄격하게 순환으로 이어지는 모든 용어에 무작위 가 나타날 수 있습니다 ! 의도 된 말장난은 아니지만 let, 게으른 언어 에서는 엄격히 더 재귀적인 바인딩이 합리적입니다. 엄격함은 또한 whereSCC를 제외하고는 상대적 효과를 주문할 방법이 전혀 없다는 사실을 악화시킵니다. SCC에 의한 경우를 제외하고는 문 수준의 구성이며 그 효과는 엄격하게 어떤 순서로든 발생할 수 있으며 순수한 언어를 사용하더라도 #f발행물. 엄격한 where로컬 문제로 코드를 수수께끼로 만듭니다.
Edward KMETT

2
게으름이 가치 제한을 피하는 데 어떻게 도움이되는지 설명해 주시겠습니까? 나는 이것을 이해할 수 없었다.
Tom Ellis

3
@PaulBone 무슨 소리 야? 게으름은 제어 구조와 관련이 있습니다. 엄격한 언어로 자신의 제어 구조를 정의하면 람다 또는 이와 유사한 것을 사용해야 할 것입니다. 때문에이 ifFunc(True, x, y)둘을 평가하는 것입니다 x그리고 y대신의 x.
세미콜론

28

일반 주문 평가와 지연 평가에는 차이가 있습니다 (하스켈에서와 같이).

square x = x * x

다음 식을 평가하는 중 ...

square (square (square 2))

... 열심히 평가 :

> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256

... 일반 주문 평가 :

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256

... 게으른 평가 :

> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256

지연 평가는 구문 트리를보고 트리 변환을 수행하기 때문입니다.

square (square (square 2))

           ||
           \/

           *
          / \
          \ /
    square (square 2)

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
        square 2

           ||
           \/

           *
          / \
          \ /
           *
          / \
          \ /
           *
          / \
          \ /
           2

... 일반 주문 평가는 텍스트 확장 만 수행합니다.

이것이 우리가 지연 평가를 사용할 때 더 강력 해지면서 (평가가 다른 전략보다 더 자주 종료 됨) 성능이 열망 평가 (적어도 O 표기법에서)와 동일한 이유입니다.


25

RAM 관련 가비지 콜렉션과 동일한 방식으로 CPU 관련 지연 평가. GC를 사용하면 메모리가 무제한 인 것처럼 가장하여 필요한만큼 메모리에있는 개체를 요청할 수 있습니다. 런타임은 사용할 수없는 개체를 자동으로 회수합니다. LE를 사용하면 계산 리소스가 무제한 인 척 할 수 있습니다. 필요한만큼 계산을 수행 할 수 있습니다. 런타임은 불필요한 (주어진 경우) 계산을 실행하지 않습니다.

이러한 "가장"모델의 실질적인 이점은 무엇입니까? 개발자가 리소스 관리에서 어느 정도까지는 해제하고 소스에서 일부 상용구 코드를 제거합니다. 그러나 더 중요한 것은보다 광범위한 컨텍스트에서 솔루션을 효율적으로 재사용 할 수 있다는 것입니다.

숫자 S와 숫자 N의 목록이 있다고 가정합니다. 목록 S에서 숫자 N에 가장 가까운 숫자 M을 찾아야합니다. 단일 N과 N의 일부 목록 L (L의 각 N에 대해 ei)의 두 가지 컨텍스트를 가질 수 있습니다. S)에서 가장 가까운 M을 찾습니다. 지연 평가를 사용하는 경우 S를 정렬하고 이진 검색을 적용하여 M에서 N에 가장 가까운 M을 찾을 수 있습니다. 좋은 지연 정렬을 위해서는 단일 N 및 O (ln (size (S)) *에 대해 O (size (S)) 단계가 필요합니다. (size (S) + size (L))) 균등 분포 L에 대한 단계. 최적의 효율성을 달성하기위한 지연 평가가없는 경우 각 컨텍스트에 대한 알고리즘을 구현해야합니다.


GC와의 비유가 약간 도움이되었지만 "일부 상용구 코드 제거"의 예를 들어 주시겠습니까?
Abdul

1
@Abdul, ORM 사용자에게 친숙한 예 : 지연 연결 로딩. DB에서 "적시에"연결을로드하고 동시에로드 할시기와 캐시 방법을 명시 적으로 지정할 필요가 없도록 개발자를 해제합니다. 다음은 또 다른 예입니다 : projectlombok.org/features/GetterLazy.html .
Alexey 2011

25

Simon Peyton Jones를 믿는다면 게으른 평가는 그 자체로 중요하지 않고 디자이너가 언어를 순수하게 유지하도록 강요하는 '헤어 셔츠'로서 만 중요 합니다. 나는이 관점에 공감한다.

Richard Bird, John Hughes 및 Ralf Hinze는 지연 평가로 놀라운 일을 할 수 있습니다. 그들의 작품을 읽으면 감사하는 데 도움이 될 것입니다. 좋은 출발점은 Bird의 웅장한 스도쿠 솔버와 Why Functional Programming Matters 에 대한 Hughes의 논문입니다 .


그것은 그냥 그들을 강제 순수한 언어를 유지하기 위해, 그것도 단지 허용 할 때 (의 도입 이전, 그들이 그렇게 할 IO의 서명이 모나드) main이 될 것입니다 String -> String그리고 당신은 이미 제대로 대화 형 프로그램을 작성할 수 있습니다.
leftaroundabout

@leftaroundabout : 엄격한 언어가 모든 효과를 IO모나드 로 강제하는 것을 막는 것은 무엇입니까 ?
Tom Ellis

13

tic-tac-toe 프로그램을 고려하십시오. 여기에는 네 가지 기능이 있습니다.

  • 현재 보드를 가져와 각각 하나의 이동이 적용된 새 보드 목록을 생성하는 이동 생성 기능입니다.
  • 그런 다음 이동 생성 기능을 적용하여이 항목에서 따를 수있는 모든 보드 위치를 유도하는 "이동 트리"기능이 있습니다.
  • 최상의 다음 동작을 찾기 위해 트리 (또는 그 일부만)를 걷는 미니 맥스 함수가 ​​있습니다.
  • 플레이어 중 한 명이 이겼는지 판단하는 보드 평가 기능이 있습니다.

이것은 관심사를 명확하게 분리합니다. 특히 이동 생성 기능과 보드 평가 기능은 게임의 규칙을 이해하는 데 필요한 유일한 기능입니다. 이동 트리와 미니 맥스 기능은 완전히 재사용 할 수 있습니다.

이제 tic-tac-toe 대신 체스를 구현해 보겠습니다. "eager"(즉, 전통적인) 언어에서는 이동 트리가 메모리에 맞지 않기 때문에 작동하지 않습니다. 따라서 이제 보드 평가 및 이동 생성 기능을 이동 트리 및 미니 맥스 논리와 혼합해야합니다. 생성 할 이동을 결정하는 데 minimax 논리를 사용해야하기 때문입니다. 우리의 멋진 모듈 식 구조가 사라졌습니다.

그러나 게으른 언어에서 이동 트리의 요소는 minimax 함수의 요구에 응답해서 만 생성됩니다. 전체 이동 트리를 생성 할 필요는 없습니다. 그래서 우리의 깨끗한 모듈 구조는 여전히 실제 게임에서 작동합니다.


1
[ "eager"(즉, 전통적인) 언어에서는 이동 트리가 메모리에 맞지 않기 때문에 작동하지 않습니다.]-Tic-Tac-Toe에게는 확실히 그렇습니다. 저장할 위치는 최대 3 ** 9 = 19683 개입니다. 각 파일을 사치스러운 50 바이트에 저장하면 거의 1 메가 바이트입니다. 그건 아무것도 아닙니다 ...
Jonas Kölker 2009-06-13

6
네, 그게 내 요점입니다. 열망하는 언어는 사소한 게임에 대해 깔끔한 구조를 가질 수 있지만 실제적인 경우에는 그 구조를 손상시켜야합니다. 게으른 언어는 그런 문제가 없습니다.
Paul Johnson

3
공정하게 말하면 게으른 평가는 자체 메모리 문제로 이어질 수 있습니다. 사람들이 왜
하스켈

@PaulJohnson 모든 직책을 평가한다면, 열심히 평가하든 게으르게 평가하든 차이가 없습니다. 동일한 작업을 수행해야합니다. 중간에 멈추고 위치의 절반 만 평가하면 두 경우 모두 작업의 절반을 수행해야하기 때문에 차이가 없습니다. 두 평가의 유일한 차이점은 알고리즘이 느리게 작성되면 더 멋지게 보인다는 것입니다.
jul

12

여기에 아직 논의에서 제기되지 않은 두 가지 사항이 더 있습니다.

  1. 게으름은 동시 환경의 동기화 메커니즘입니다. 일부 계산에 대한 참조를 만들고 그 결과를 여러 스레드간에 공유 할 수있는 가볍고 쉬운 방법입니다. 여러 스레드가 평가되지 않은 값에 액세스하려고하면 그중 하나만 실행하고 나머지 스레드는 그에 따라 차단하여 값이 사용 가능 해지면 수신합니다.

  2. 게으름은 순수한 환경에서 데이터 구조를 분할하는 데 기본이됩니다. 이것은 Okasaki가 Purely Functional Data Structures 에서 자세히 설명하지만, 기본 아이디어는 lazy 평가가 특정 유형의 데이터 구조를 효율적으로 구현하는 데 중요한 제어 된 형태의 돌연변이라는 것입니다. 우리가 순결한 헤어 셔츠를 입도록 강요하는 게으름에 대해 자주 이야기하지만, 다른 방법도 적용됩니다. 시너지 효과가있는 한 쌍의 언어 기능입니다.


10

컴퓨터를 켜고 Windows가 Windows 탐색기에서 하드 드라이브의 모든 단일 디렉토리를 열지 않고 특정 디렉토리가 필요하거나 특정 프로그램이 필요하다는 것을 표시 할 때까지 컴퓨터에 설치된 모든 단일 프로그램을 실행하지 않는 경우, "게으른"평가입니다.

"지연"평가는 필요할 때 작업을 수행합니다. 프로그래밍 언어 또는 라이브러리의 기능 일 때 유용합니다. 일반적으로 모든 것을 미리 계산하는 것보다 직접 지연 평가를 구현하는 것이 더 어렵 기 때문입니다.


1
어떤 사람들은 그것이 정말로 "게으른 실행"이라고 말할 수 있습니다. Haskell과 같은 합리적으로 순수한 언어를 제외하고는 그 차이는 정말 중요하지 않습니다. 그러나 차이점은 계산이 지연 될뿐만 아니라 이와 관련된 부작용 (파일 열기 및 읽기 등)도 있다는 것입니다.
오웬

8

이걸 고려하세요:

if (conditionOne && conditionTwo) {
  doSomething();
}

doSomething () 메서드는 conditionOne이 true 이고 conditionTwo가 true 인 경우에만 실행됩니다 . conditionOne이 거짓 인 경우 왜 conditionTwo의 결과를 계산해야합니까? 이 경우 conditionTwo의 평가는 시간 낭비입니다. 특히 조건이 일부 메서드 프로세스의 결과 인 경우 더욱 그렇습니다.

이것이 게으른 평가 관심의 한 예입니다 ...


게으른 평가가 아니라 단락이라고 생각했습니다.
Thomas Owens

2
conditionTwo는 실제로 필요한 경우 (즉 conditionOne이 참인 경우)에만 계산되므로 지연 평가입니다.
Romain Linsolas

7
단락은 게으른 평가의 퇴보 적 사례라고 생각하지만 확실히 생각하는 일반적인 방법은 아닙니다.
rmeador

19
단락은 실제로 지연 평가의 특별한 경우입니다. 게으른 평가는 분명히 단락 이상의 것을 포함합니다. 아니면 단락이 게으른 평가 이상으로 무엇을 가지고 있습니까?
yfeldblum

2
@Juliet : 당신은 '게으른'에 대한 강한 정의를 가지고 있습니다. 두 개의 매개 변수를 사용하는 함수의 예는 단락 if 문과 동일하지 않습니다. 단락 if 문은 불필요한 계산을 방지합니다. 귀하의 예제에 대한 더 나은 비교는 Visual Basic의 연산자 "andalso"가 두 조건을 모두 평가하도록하는 것입니다

8
  1. 효율성을 높일 수 있습니다. 이것은 명백해 보이지만 실제로 가장 중요한 것은 아닙니다. (참고도 게으름 수 죽일 너무 효율이 -이 사실을 즉시 명확하지 않다 그러나, 일시적인 결과를 많이를 저장하는 것이 아니라 즉시를 계산하여, 당신은 RAM의 엄청난 금액을 사용할 수 있습니다..)

  2. 이를 통해 언어로 하드 코딩되지 않고 일반 사용자 수준 코드에서 흐름 제어 구문을 정의 할 수 있습니다. (예 : Java에는 for루프가 있고, Haskell에는 for함수가 있습니다. Java에는 예외 처리 기능이 있으며, Haskell에는 다양한 유형의 예외 모나드가 있습니다. C #에는 있고 goto, Haskell에는 연속 모나드가 있습니다 ...)

  3. 생성 할 데이터의 을 결정 하는 알고리즘에서 데이터 생성 알고리즘을 분리 할 수 ​​있습니다 . 개념적으로 무한한 결과 목록을 생성하는 함수 하나와 필요한만큼이 목록을 처리하는 다른 함수를 작성할 수 있습니다. 요컨대, 5 개의 생성기 함수와 5 개의 소비자 함수를 가질 수 있으며 한 번에 두 작업을 결합하는 5 x 5 = 25 함수를 수동으로 코딩하는 대신 모든 조합을 효율적으로 생성 할 수 있습니다. (!) 우리 모두는 디커플링이 좋은 것임을 압니다.

  4. 다소간 순수한 기능적 언어 를 디자인하도록 강요합니다 . 항상 지름길을 사용하는 것은 유혹적이지만 게으른 언어에서는 약간의 불순물이 코드를 만듭니다. 격렬 바로 가기를 복용에 대한 어떤 강하게을 방해 할, 예측할 수없는.


6

게으름의 큰 이점 중 하나는 합리적인 상각 된 경계로 변경 불가능한 데이터 구조를 작성할 수 있다는 것입니다. 간단한 예는 변경 불가능한 스택 (F # 사용)입니다.

type 'a stack =
    | EmptyStack
    | StackNode of 'a * 'a stack

let rec append x y =
    match x with
    | EmptyStack -> y
    | StackNode(hd, tl) -> StackNode(hd, append tl y)

코드는 합리적이지만 두 개의 스택 x와 y를 추가하면 최상의 경우, 최악의 경우 및 평균적인 경우 O (x 길이) 시간이 걸립니다. 두 개의 스택을 추가하는 것은 모 놀리 식 작업이며 스택 x의 모든 노드에 연결됩니다.

데이터 구조를 지연 스택으로 다시 작성할 수 있습니다.

type 'a lazyStack =
    | StackNode of Lazy<'a * 'a lazyStack>
    | EmptyStack

let rec append x y =
    match x with
    | StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
    | Empty -> y

lazy생성자에서 코드 평가를 일시 중단하여 작동합니다. 를 사용하여 평가되면 .Force()반환 값이 캐시되고 이후에 다시 사용됩니다..Force() .

lazy 버전에서 추가는 O (1) 작업입니다. 1 개의 노드를 반환하고 목록의 실제 재 구축을 일시 중단합니다. 이 목록의 헤드를 얻으면 노드의 내용을 평가하여 헤드를 강제로 반환하고 나머지 요소로 하나의 서스펜션을 생성하므로 목록의 헤드를 가져 오는 것은 O (1) 작업입니다.

따라서 우리의 게으른 목록은 지속적으로 재 구축 상태에 있으며 모든 요소를 ​​탐색 할 때까지이 목록을 재 구축하는 데 드는 비용을 지불하지 않습니다. laziness를 사용하여이 목록은 O (1) consing 및 appending을 지원합니다. 흥미롭게도 노드에 액세스 할 때까지 노드를 평가하지 않기 때문에 잠재적으로 무한한 요소로 목록을 구성 할 수 있습니다.

위의 데이터 구조는 각 순회에서 노드를 다시 계산할 필요가 없으므로 .NET의 바닐라 IEnumerables와 뚜렷하게 다릅니다.


5

이 스 니펫은 lazy 평가와 not lazy 평가의 차이점을 보여줍니다. 물론이 피보나치 함수는 자체적으로 최적화 될 수 있고 재귀 대신 지연 평가를 사용할 수 있지만 이는 예제를 망칠 것입니다.

하자 우리가 가정 필요한 경우에만 게으른 평가와 함께 그들이 생성됩니다, 모든 20 개 숫자가 선행 생성 할 필요가 없습니다 게으른 평가와 함께, 뭔가를 20 개 첫 번째 번호를 사용해야하지만. 따라서 필요한 경우 계산 가격 만 지불합니다.

샘플 출력

지연 생성이 아님 : 0.023373
지연 생성 : 0.000009
지연 출력이 아님 : 0.000921
지연 출력 : 0.024205
import time

def now(): return time.time()

def fibonacci(n): #Recursion for fibonacci (not-lazy)
 if n < 2:
  return n
 else:
  return fibonacci(n-1)+fibonacci(n-2)

before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()


before3 = now()
for i in notlazy:
  print i
after3 = now()

before4 = now()
for i in lazy:
  print i
after4 = now()

print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)

5

지연 평가는 데이터 구조에서 가장 유용합니다. 구조의 특정 점만 지정하고 전체 배열의 관점에서 다른 모든 점을 표현하는 방식으로 배열 또는 벡터를 정의 할 수 있습니다. 이를 통해 매우 간결하고 높은 런타임 성능으로 데이터 구조를 생성 할 수 있습니다.

이것이 실제로 작동하는지 확인하려면 instinct 라는 신경망 라이브러리를 살펴볼 수 있습니다 . 우아함과 고성능을 위해 게으른 평가를 많이 사용합니다. 예를 들어 저는 전통적으로 명령적인 활성화 계산을 완전히 제거했습니다. 단순한 게으른 표현이 나를 위해 모든 것을 수행합니다.

이것은 예를 들어 활성화 함수 와 역 전파 학습 알고리즘에서 사용됩니다 (2 개의 링크 만 게시 할 수 있으므로 모듈 에서 learnPat함수를 AI.Instinct.Train.Delta직접 찾아봐야합니다 ). 전통적으로 둘 다 훨씬 더 복잡한 반복 알고리즘이 필요합니다.


4

다른 사람들은 이미 모든 큰 이유를 제시했지만 게으름이 중요한 이유를 이해하는 데 도움이되는 유용한 연습 은 엄격한 언어로 고정 소수점 함수를 작성하는 것입니다.

Haskell에서 고정 소수점 함수는 매우 쉽습니다.

fix f = f (fix f)

이것은 확장

f (f (f ....

그러나 Haskell은 게으 르기 때문에 무한한 계산 체인은 문제가되지 않습니다. 평가는 "외부에서 내부로"수행되며 모든 것이 훌륭하게 작동합니다.

fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)

중요한 fix것은 게 으르는 것이 아니라 게 으르는 것이 중요 f합니다. 이미 엄격한을 받으면 f손을 공중에 던지고 포기하거나 eta 확장하여 물건을 정리할 수 있습니다. (이것은 언어가 아니라 엄격 / 게으른 라이브러리 에 대해 Noah가 말한 것과 매우 유사 합니다).

이제 엄격한 Scala에서 동일한 함수를 작성한다고 상상해보십시오.

def fix[A](f: A => A): A = f(fix(f))

val fact = fix[Int=>Int] { f => n =>
    if (n == 0) 1
    else n*f(n-1)
}

물론 스택 오버플로가 발생합니다. 작동하게하려면 필요에 따라 f인수 를 만들어야 합니다.

def fix[A](f: (=>A) => A): A = f(fix(f))

def fact1(f: =>Int=>Int) = (n: Int) =>
    if (n == 0) 1
    else n*f(n-1)

val fact = fix(fact1)

3

나는 당신이 현재 어떻게 생각하는지 모르겠지만, 게으른 평가를 언어 기능보다는 도서관 문제로 생각하는 것이 유용하다고 생각합니다.

엄격한 언어에서는 몇 가지 데이터 구조를 구축하여 게으른 평가를 구현할 수 있으며 게으른 언어 (적어도 Haskell)에서는 원할 때 엄격함을 요청할 수 있습니다. 따라서 언어 선택은 실제로 프로그램을 지연 또는 비 지연으로 만드는 것이 아니라 기본적으로 얻는 프로그램에 영향을줍니다.

그렇게 생각하면 나중에 데이터를 생성하는 데 사용할 수있는 데이터 구조를 작성하는 모든 위치를 생각하면 (그 전에 너무 많이 보지 않고) lazy에 대한 많은 용도를 볼 수 있습니다. 평가.


1
엄격한 언어로 지연 평가를 구현하는 것은 종종 Turing Tarpit입니다.
itsbruce

2

내가 사용한 게으른 평가의 가장 유용한 활용은 일련의 하위 기능을 특정 순서로 호출하는 함수였습니다. 이러한 하위 함수 중 하나가 실패하면 (거짓 반환) 호출하는 함수는 즉시 반환해야합니다. 그래서 나는 이렇게 할 수 있었다.

bool Function(void) {
  if (!SubFunction1())
    return false;
  if (!SubFunction2())
    return false;
  if (!SubFunction3())
    return false;

(etc)

  return true;
}

또는 더 우아한 솔루션 :

bool Function(void) {
  if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
    return false;
  return true;
}

사용을 시작하면 점점 더 자주 사용할 수있는 기회를 보게 될 것입니다.


2

게으른 평가가 없으면 다음과 같이 작성할 수 없습니다.

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

음, 이모, 그렇게하는 것은 나쁜 생각입니다. 이 코드는 정확할 수 있지만 (목표 달성에 따라 다름) 읽기가 어렵습니다. 이는 항상 나쁜 일입니다.
Brann

12
나는 그렇게 생각하지 않는다. C와 그 친척의 표준 구조입니다.
Paul Johnson

이것은 지연 평가가 아닌 단락 평가의 예입니다. 아니면 그게 사실상 같은 것일까 요?
RufusVS

2

무엇보다도 게으른 언어는 다차원 무한 데이터 구조를 허용합니다.

scheme, python 등은 스트림이있는 1 차원 무한 데이터 구조를 허용하지만 한 차원 만 따라 이동할 수 있습니다.

게으름은 동일한 프린지 문제에 유용 하지만 해당 링크에 언급 된 코 루틴 연결에 주목할 가치가 있습니다.


2

게으른 평가는 가난한 사람의 방정식 추론입니다 (이상적으로는 관련된 유형 및 작업의 속성에서 코드의 속성을 추론하는 것으로 예상 할 수 있음).

잘 작동하는 예 : sum . take 10 $ [1..10000000000]. 하나의 직접적이고 간단한 숫자 계산 대신에 10 개의 숫자의 합계로 줄여도 괜찮습니다. 당연히 게으른 평가가 없으면 처음 10 개의 요소를 사용하기 위해 메모리에 거대한 목록이 생성됩니다. 확실히 매우 느리고 메모리 부족 오류가 발생할 수 있습니다.

우리가 원하는만큼 좋지 않은 예 : sum . take 1000000 . drop 500 $ cycle [1..20]. 목록이 아닌 루프에 있더라도 실제로 1000000 개의 숫자를 합산합니다. 여전히이 해야 몇 조건문과 몇 가지 수식, 하나의 직접 수치 계산을 줄일 수. 1000000 숫자를 합산하는 보다 훨씬 낫습니다. 루프에 있고 목록에없는 경우에도 (즉, 삼림 벌채 최적화 이후).


또 다른 점은 코드에 대한 것이 가능하게된다 꼬리 재귀 모듈로 죄수의 스타일, 그리고 그것은 단지 작동합니다 .

cf. 관련 답변 .


1

"지연 평가"가 다음과 같이 결합 된 부울에서와 같이 의미하는 경우

   if (ConditionA && ConditionB) ... 

답은 단순히 프로그램이 소비하는 CPU주기가 적을수록 더 빨리 실행된다는 것입니다. 시간의) 어쨌든 그들을 수행하기 위해 ...

만약 otoh, 당신은 내가 "게으른 이니셜 라이저"로 알려진 것을 의미합니다.

class Employee
{
    private int supervisorId;
    private Employee supervisor;

    public Employee(int employeeId)
    {
        // code to call database and fetch employee record, and 
        //  populate all private data fields, EXCEPT supervisor
    }
    public Employee Supervisor
    { 
       get 
          { 
              return supervisor?? (supervisor = new Employee(supervisorId)); 
          } 
    }
}

음,이 기술은 Employee 개체를 사용하는 클라이언트가 감독자의 데이터에 액세스해야하는 경우를 제외하고는 클래스를 사용하는 클라이언트 코드를 통해 Supervisor 데이터 레코드에 대한 데이터베이스를 호출 할 필요가 없도록합니다. 이렇게하면 Employee를 인스턴스화하는 프로세스가 더 빨라집니다. 그러나 Supervisor가 필요할 때 Supervisor 속성에 대한 첫 번째 호출은 Database 호출을 트리거하고 데이터를 가져와 사용할 수 있습니다.


0

고차 함수 에서 발췌

3829로 나눌 수있는 100,000 미만의 가장 큰 숫자를 찾아 보겠습니다.이를 위해 우리는 해결책이 있다는 것을 알고있는 가능성 집합을 필터링합니다.

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

먼저 100,000 미만의 모든 숫자 목록을 내림차순으로 만듭니다. 그런 다음 술어로 필터링하고 숫자가 내림차순으로 정렬되기 때문에 술어를 충족하는 가장 큰 숫자가 필터링 된 목록의 첫 번째 요소입니다. 시작 세트에 유한 목록을 사용할 필요조차 없었습니다. 그것은 다시 행동의 게으름입니다. 필터링 된 목록의 헤드 만 사용하기 때문에 필터링 된 목록이 유한인지 무한인지는 중요하지 않습니다. 첫 번째 적절한 솔루션을 찾으면 평가가 중지됩니다.

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