느긋한 계산법

컴퓨터 프로그래밍에서 **느긋한 계산법**(Lazy evaluation)은 계산의 결과값이 필요할 때까지 계산을 늦추는 기법이다. 두 가지 관련된 항목들이 있는데 지연 계산법과 최소 계산법이다.

느긋하게 계산하면 필요없는 계산을 하지 않으므로 실행을 더 빠르게 할 수 있고, 복합 수식을 계산할 때 오류 상태를 피할 수 있고, 무한 자료 구조를 쓸 수 있고, 미리 정의된 것을 이용하지 않고 보통 함수로 제어 구조를 정의할 수 있다.

느긋한 계산법을 사용하는 언어들은 "이름으로 호출"하거나 "필요할 때 호출"하는 계산 전략을 사용하는 것으로 나눌 수 있다. 하스켈과 같은 대부분의 실제 느긋한 언어들은 성능상의 이유로 "필요할 때 호출" 전략을 사용한다. 그러나 느긋한 계산법을 이론적으로 표현한 것들은 간편하게 "이름으로 호출"하기도 한다.

느긋한 계산법의 반대되는 계산법은 조급한 계산법이 있는데 엄격한 계산법이라고도 한다. 조급한 계산법은 대부분의 프로그래밍 언어가 동작하는 방식이다.

## 복합 수식
*(a *'and** b)''와 같은 논리식을 계산한다고 하면 ''a''항이 **거짓**인 경우에, ''b''항을 계산하지 않아도 전체 식의 답을 알 수 있다. ''(a **or** b)''에서 ''a''항이 **참**인 경우에도 마찬가지이다. 여기서 항이 복잡한 식이면 이점이 많고, 식에서 결과가 **참**이나 **거짓**일 가능성과 계산의 비용에 따라 어떤 항이 먼저 계산되어야 좋은지 알 수 있다. 따라서 ''(a **or** b **or** c)''와 같은 식에서 ''a''항이 **참**값을 가질 가능성이 많다면, 전체 식을 쉽게 계산할 수 있다. 이런 가능성을 보장하기 위해, 컴파일러는 더 계산해야 할 것인지, 다른 항을 [[지름길 계산]] 해야 할 것인지를 검사하기도 한다. 이런 검사는 계산을 줄이는 것을 실패할 경우나 꼭 필요한 경우 무조건 전체 식을 계산해야 할 때 시간이 더 많이 걸리게 된다.

산술식을 계산할 때에도 유사한 접근 방식을 취할 수 있다. 어떤 수에 0을 더하는 것은 의미 없고, 0이나 1을 곱하는 경우에는 특별한 효과가 있다. 그러나 이런 특별한 값들을 검사하는 것은 논리값은 2개의 값을 가지는 데 비하여 수는 여러 가지 값을 가질 수 있기 때문에 큰 이득이 되지 못한다. 그러나 0이 소수점 값으로 변환하는 데 쓰이지 않는 이상 하드웨어가 특수한 경우는 잘 계산하고 있다.

==오류 회피==
특정한 상황에서는 계산되면 오류 상황이 되어 오류를 발생시키는 수식이 있다고 가정하자. 그 수식은 ''(계산해도 안전한 식 **and** 수식)''과 같은 형태로 보호되어 있을 수도 있다. 예를 들면 <code>(N <> 0 and (x mod N = 0)</code>과 같은 형태이다. 분명히, 이것은 <code>if N <> 0 then if x mod N = 0 then ...</code>과 같은 형태로 쓸 수 있지만 깔끔하지 못하다. 반대로, <code>(a **or** b)</code>로 검사할 때, ''a''가 **참*'인 경우에 *b*는 계산될 필요가 없을뿐더러 문제를 일으킬 수도 있다. *a*와 *b* 둘 중 하나가 참인 경우에 어떤 행동을 하고 싶다면(이 식에서 의미하는 것이다!) 친절하게 <code>if a then action else if b then action</code>이라고 쓸 수 있지만, action을 반복해서 쓰기 때문에 실수를 할 가능성이 있다.

좀 더 구체적으로 살펴보자. 여기 공백이 뒤쪽에 있는 텍스트 문자열이 있고, 공백이 아닌 맨 마지막 문자의 위치를 알고 싶다고 할 때,
<pre>
  l:=Length(t);
  while l > 0 and t(l) = " " do decrement l;
</pre>
라고 하거나, 좀 더 유연한 언어에서는
<pre>
  for l:=Length(t):1:-1 while t(l) = " " do;
</pre>
라고 할 수 있다. 분명히, 문자열 전체가 공백으로 차 있다면 *l*은 0이 될 것이다. 그러나 문자열에서 0번째 문자열을 읽는 시도는 하지 않는다. 이것은 잘해야 쓸데 없는 노력이고, 못하면 오류를 일으킬 수도 있다.

==지연 계산법==
지연 계산법은 [[함수형 프로그래밍 언어]]에서 주로 사용한다. 지연 계산법을 사용하면 수식이 변수에 접근하는 순간 계산되는 것이 아니라, 강제로 계산값을 출력하라고 할 때까지 계산을 늦춘다. 말하자면, *x:=수식;*과 같은 구문이 있다고 하면, 분명히 수식을 계산하여 결과값을 *x*에 넣어야 한다. 하지만 *x*에 들어있는 값은 나중에 참조되어 그 값을 요구한다든지 할 때까지는 상관이 없다. 비록 다른 심볼보다 바깥 세상을 보고자 하는 몇 가지 심볼을 생성하기 위해 빠르게 자라는 의존성 나무의 가지를 칠 수 있다고 할지라도, 식의 계산을 뒤로 미룰 수 있다.

수식의 계산을 기본으로 늦추는 프로그래밍 언어도 있고 [[함수 (프로그래밍)|함수]]나 특별한 구문으로 계산을 늦추는 언어도 있다. [[미란다 프로그래밍 언어|미란다]]와 하스켈은 기본으로 계산을 늦춘다. 다른 언어들은 특수한 구문을 써서 계산을 늦추라고 명시하거나 [[성크]]에 수식을 감싸서 계산을 늦춘다. 예를 들어 [[스킴 (프로그래밍 언어)|스킴]]에서는 "delay"와 "force"를 써서 이런 것을 명시할 수 있다.

지연 계산법은 계산할 때 무한 반복 문제나 크기 문제 없이 계산 가능한 끝없는 목록을 만들 수 있다. 예를 들어, [[피보나치 수]]로 (보통 [[스트림 (컴퓨팅)|스트림]]이라고 부르는) 끝없는 목록을 만드는 함수를 작성할 수 있다. *n''번째 피보나치 수는 끝없는 목록의 원소를 추출하는 것만으로도 간단하게 계산할 수 있는데, 목록의 처음 n개의 수들만 계산하도록 하면서 할 수 있다.

예를 들어 하스켈에서는 모든 피보나치 수의 목록을 다음과 같이 쓸 수 있다.

```haskell
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
```

하스켈의 구문 "`:`"는 목록에 원소를 첨가하는 것이고 `tail`은 첫 번째 원소를 빼고 리스트를 돌려주며, `zipWith`는 특정한 (여기서는 (+)) 함수를 사용하여 두 목록의 각 원소를 결합하여 세 번째 항목을 생성하도록 한다.

프로그래머가 주의한다면, 특정한 결과값을 생성할 때 필요한 값만 계산할 것이다. 그러나 특정 계산이 목록의 길이를 구하는 것이라든지, 목록의 원소들의 전체합을 구하는 것과 같이 프로그램에서 무한히 많은 원소를 계산하는 것이라면 계산을 끝내지 못하거나 메모리를 다 쓰게 될 것이다.

## 같이 보기
* 최소 계산법
* 비엄격 프로그래밍 언어
* 함수형 프로그래밍
* 자료 흐름
* 람다 대수
* 결합 논리
* 커리화
* 그래프 감축
* 콘 프로그래밍 언어

비교:
* 느긋한 초기화
* 예측 계산

## 참고문서
* Steve Summit's C FAQs

## 외부 링크
* Functional programming in Python becomes lazy

분류:평가 전략
분류:프로그래밍 패러다임
분류:프로그래밍 계산법
분류:컴파일러 최적화