지연 평가

지연 평가 (Delayed Evaluation)

프로미스(promise)는 평가를 필요할 때까지 미루는 값을 감싸는 메커니즘이에요. 이 문서에서는 racket/promise 라이브러리가 제공하는 delay, force, 그리고 다양한 종류의 프로미스를 설명합니다.

출처: Racket Reference

본문

(require racket/promise) package: base

이 섹션에 문서화된 바인딩들은 racket/promiseracket 라이브러리가 제공합니다. racket/base는 제공하지 않아요.

프로미스는 force를 통해 요청 시(on demand) 평가될 표현식을 감쌉니다. 프로미스가 강제(force)된 후에는, 그 프로미스에 대한 이후의 모든 force가 같은 결과를 만들어 냅니다.

procedure
(promise? v) → boolean?
v : any/c

v가 프로미스이면 #t, 그렇지 않으면 #f를 반환합니다.

syntax
(delay body ...+)

강제되면 body들을 평가해서 값을 만들어 내는 프로미스를 생성합니다. 그 결과는 캐시되어, 이후의 force 사용은 캐시된 값을 즉시 만들어 냅니다. 여기에는 여러 값과 예외도 포함됩니다.

syntax
(lazy body ...+)

delay와 비슷하지만, 마지막 body가 강제될 때 프로미스를 만들어 내면 그 프로미스도 값을 얻기 위해 강제됩니다. 다시 말해, 이 폼은 합성 가능한(composable) 프로미스를 만듭니다. 여기서 body의 계산이 이어지는 프로미스의 계산에 "붙어 있고", 단일 force가 각 단계를 꼬리 호출(tail-call)하며 전체 체인을 반복합니다.

이 폼의 마지막 body는 단일 값을 만들어 내야 하지만, 그 값 자체는 여러 값을 반환하는 delay 프로미스일 수 있습니다.

lazy 폼은 지연 라이브러리와 언어를 구현할 때 유용한데, 꼬리 호출을 프로미스로 감쌀 수 있기 때문이에요.

procedure
(force v) → any
v : any/c

v가 프로미스이면, 값을 얻기 위해 프로미스가 강제됩니다. 프로미스가 이전에 강제된 적이 없다면, 그 결과는 프로미스에 기록되어 이후의 강제가 같은 값(들)을 만들어 냅니다. 프로미스를 강제하는 것이 예외를 일으키면, 그 예외도 유사하게 기록되어 프로미스를 강제할 때마다 같은 예외가 일어납니다.

원래의 force 호출이 반환되기 전에 v가 다시 강제되면, exn:fail 예외가 일어납니다.

v가 프로미스가 아니라면, 그것이 결과로 그대로 반환됩니다.

procedure
(promise-forced? promise) → boolean?
promise : promise?

promise가 강제된 적이 있으면 #t를 반환합니다.

procedure
(promise-running? promise) → boolean?
promise : promise?

promise가 현재 강제되는 중이면 #t를 반환합니다. (프로미스는 running이거나 forced일 수 있지만 둘 다일 수는 없습니다.)

추가적인 프로미스 종류 (Additional Promise Kinds)

syntax
(delay/name body ...+)

delay 프로미스와 비슷한 "call-by-name" 프로미스를 생성하지만, 결과 값이 캐시되지는 않습니다. 이런 종류의 프로미스는 본질적으로 force가 인식하는 방식으로 감싸진 썽크(thunk)입니다.

delay/name 프로미스가 스스로를 강제하면 예외가 일어나지 않으며, promise-running?promise-forced?의 의미에서 그 프로미스는 결코 "running"이나 "forced"로 간주되지 않습니다.

procedure
(promise/name? promise) → boolean?
promise : any/c

promisedelay/name으로 만들어진 프로미스이면 #t를 반환합니다.

base 패키지의 6.3 버전에서 추가됨.

syntax
(delay/strict body ...+)

"엄격한(strict)" 프로미스를 생성합니다: 즉시 평가되며, 그 결과가 프로미스 값으로 감싸집니다. body가 여러 값으로 평가될 수 있으며, 결과 프로미스를 강제하면 이 값들이 반환된다는 점에 유의하세요.

syntax
(delay/sync body ...+)

프로미스를 만들어 냅니다. 여기서 프로미스를 현재 실행 중인 스레드 이외의 스레드가 강제하려고 하면, 결과가 나올 때까지 force가 블록합니다. 이런 종류의 프로미스는 sync와 함께 사용하기 위한 동기화 가능한 이벤트(synchronizable event)이기도 합니다. 프로미스를 동기화하는 것은 그것을 강제하지는 않고, 다른 스레드가 값을 강제할 때까지 기다리기만 합니다. 동기화 결과는 #<void>입니다.

delay/sync로 만들어진 프로미스가 이미 그 프로미스를 실행 중인 스레드에서 강제되면, delay로 만들어진 프로미스와 같은 방식으로 예외가 일어납니다.

syntax
(delay/thread body/option ...+)

body/option = body
            | #:group thread-group-expr

delay/sync와 비슷하지만, 새로 생성된 스레드에서 즉시 계산을 시작합니다. 스레드는 thread-group-expr이 지정한 스레드 그룹 아래에서 생성되는데, 기본값은 (make-thread-group)입니다. #:group 지정은 최대 한 번 나타날 수 있습니다.

body들이 일으키는 예외는 평소처럼 잡혀서 프로미스가 강제될 때만 일어납니다. delay/sync와 달리, body를 실행하는 스레드가 결과나 예외를 만들지 않고 종료되면, 프로미스의 force는 (블록하는 대신) 예외를 일으킵니다.

syntax
(delay/idle body/option ...+)

body/option = body
            | #:wait-for wait-evt-expr
            | #:work-while while-evt-expr
            | #:tick tick-secs-expr
            | #:use use-ratio-expr

delay/thread와 비슷하지만 다음 차이점이 있습니다:

  • wait-evt-expr이 만든 이벤트가 준비될 때까지 계산이 시작되지 않습니다. 기본값은 (system-idle-evt)입니다.
  • 계산 스레드는 while-evt-expr이 결정한 대로 프로세스가 그 외에는 유휴 상태일 때만 작업을 합니다. 이것 역시 기본값은 (system-idle-evt)입니다.
  • 스레드는 주기적으로만 실행이 허용됩니다: 매 tick-secs-expr(기본값 0.2) 초마다, 스레드는 use-ratio-expr(기본값 0.12) 비율만큼 실행이 허용됩니다; 즉 스레드는 (* tick-secs-expr use-ratio-expr) 초 동안 실행됩니다.

계산이 끝나기 전에 프로미스가 강제되면, 이벤트를 기다리거나 주기적으로 평가를 제한하지 않고 나머지 계산을 즉시 실행합니다.

#:wait-for, #:work-while, #:tick, #:use 지정은 각각 최대 한 번 나타날 수 있습니다.

(for/list/concurrent maybe-group (for-clause ...)
  body-or-break ... body)

maybe-group =


| #:group thread-group-expr thread-group-expr : thread-group?

for/list처럼 반복하지만, (#:break#:final 절 뒤에 오는) body들은 delay/thread로 감싸집니다. 각 프로미스는 결과 리스트가 반환되기 전에 강제됩니다.

스레드는 thread-group-expr 아래에서 생성되는데, 기본값은 (make-thread-group)입니다. 선택적인 #:group 절이 제공될 수 있으며, 그 경우 스레드들은 그 스레드 그룹 아래에서 생성됩니다.

이 폼은 여러 값을 반환하는 것을 지원하지 않습니다.

예시:

> (time
   (for/list/concurrent ([i (in-range 5)])
     (define duration (/ 1.0 (random 50 100)))
     (sleep duration)
     (printf "thread ~a slept for ~a milliseconds~n" i (truncate (* duration 1000)))
     i))
thread 2 slept for 10.0 milliseconds
thread 3 slept for 10.0 milliseconds
thread 1 slept for 13.0 milliseconds
thread 4 slept for 17.0 milliseconds
thread 0 slept for 18.0 milliseconds
cpu time: 47 real time: 18 gc time: 0
'(0 1 2 3 4)

base 패키지의 8.6.0.4 버전에서 추가됨.

(for*/list/concurrent maybe-group (for-clause ...)
  body-or-break ... body)

for/list/concurrent와 같지만, for*/list의 암묵적 중첩을 갖습니다.

base 패키지의 8.6.0.4 버전에서 추가됨.

더 알아보기