do / do* — 조건이 참인 동안 반복하는 범용 루프

do / do* — 조건이 참인 동안 반복하는 범용 루프

반복할 횟수를 미리 알 수 없는 경우가 있어요. 리스트의 길이만큼, 또는 특정 조건이 만족될 때까지 돌려야 할 때 dolistdotimes 같은 단순 루프로는 부족하죠. do는 시작값과 다음 값 계산식을 각 변수에 지정해 두고, 종료 조건이 참이 될 때까지 순회하는 범용 반복 매크로예요.

출처: CLHS 공식 문서 - DO, DO*

본문

do는 종료 조건이 유지되는 동안 문장들을 반복 실행해요. 반복 변수는 원하는 만큼 만들 수 있고, init-form으로 초기값을, step-form으로 매 순회마다 갱신할 값을 지정해요.

do ({var | (var [init-form [step-form]])}*) (end-test-form result-form*) declaration* {tag | statement}*
=> result*

do* ({var | (var [init-form [step-form]])}*) (end-test-form result-form*) declaration* {tag | statement}*
=> result*

첫 순회 전에 모든 init-form이 평가되고 각 변수가 자기 값에 묶여요. 이건 대입이 아니라 **묶음(binding)**이라, 루프가 끝나면 변수들의 옛 값이 복원돼요. dodo*의 차이는 갱신을 병렬로 하느냐 순차로 하느냐예요. do는 모든 step-form이 평가된 뒤에야 변수들을 갱신해서, 어떤 step 식이든 항상 모든 변수의 옛 값을 볼 수 있어요(마치 psetq처럼). 반면 do*는 첫 step을 평가·대입하고, 다음 step을 평가·대입하는 식으로 순서대로 진행돼요(마치 setq처럼).

(do ((temp-one 1 (1+ temp-one))     ; temp-one 은 병렬로 갱신
     (temp-two 0 (1- temp-two)))
    ((> (- temp-one temp-two) 5) temp-one))
=>  4

(do ((temp-one 1 (1+ temp-one))
     (temp-two 0 (1+ temp-one)))    ; do: temp-two 의 step 이 옛 temp-one 을 봄
    ((= 3 temp-two) temp-one))
=>  3

(do* ((temp-one 1 (1+ temp-one))
      (temp-two 0 (1+ temp-one)))   ; do*: temp-two 의 step 이 새 temp-one 을 봄
     ((= 3 temp-two) temp-one))
=>  2

첫 예제보다 두 번째와 세 번째 예제가 dodo*의 차이를 정확히 보여줘요. 두 번째(do)에서는 갱신이 병렬이라 temp-two의 step 계산이 갱신 전의 temp-one(1)을 보고, 세 번째(do*)에서는 순차라 갱신된 temp-one(2)을 봐요. 그래서 결과가 3과 2로 갈리죠.

각 순회의 시작에서 변수를 처리한 뒤 end-test-form이 평가돼요. 결과가 거짓이면 본문을 실행하고, 참이면 result-forms를 순서대로 평가한 뒤 루프를 끝내요. 종료 조건이 nil이면 조건이 절대 참이 되지 않으니, 본문을 끝없이 반복하는 "무한 루프"(return 등으로 탈출)를 만드는 관용 표현이 돼요.

(do ((j 0 (+ j 1)))
    (nil)                            ; Do forever.
  (format t "~%Input ~D:" j)
  (let ((item (read)))
    (if (null item) (return)         ; NIL 을 만날 때까지 처리
        (format t "~&Output ~D: ~S" j item))))
>>  Input 0: banana
>>  Output 0: BANANA
>>  Input 1: (57 boxes)
>>  Output 1: (57 BOXES)
>>  Input 2: NIL
=>  NIL

do 본문은 암묵적 tagbody예요. go의 대상이 될 tag를 본문에 둘 수 있고, return은 어느 시점에서든 즉시 루프를 빠져나가게 해요. 반복 변수를 쓰는 전형적인 응용 하나를 볼게요. 두 리스트를 병렬로 훑으며 값을 모으는 아래 형태는 (mapcar #'f foo bar)와 같은 일을 해요.

(do ((x foo (cdr x))
     (y bar (cdr y))
     (z '() (cons (f (car x) (car y)) z)))
    ((or (null x) (null y))
     (nreverse z)))

중첩 반복도 당연히 됩니다. 리스트의 리스트 구조에서 원하는 심볼을 찾는 조회 함수를 do 두 개로 만들 수 있어요.

(defun ribcage-lookup (sym ribcage)
  (do ((r ribcage (cdr r)))
      ((null r) nil)
    (do ((s (caar r) (cdr s))
         (v (cdar r) (cdr v)))
        ((null s))
      (when (eq (car s) sym)
        (return-from ribcage-lookup (car v))))))
=>  RIBCAGE-LOOKUP

함께 보기

다른 반복 함수(dolist, dotimes, loop)와 더 기본적인 구성요소(tagbody, go, block, return, let, setq)