(Lispex)sicp.io
4.12 · 내부 정의 끌어내기

평가기는 지역 초기화를 명시적인 형태로 바꿀 수 있다.

프로시저 본문 변환은 내부 이름을 모두 먼저 만들고 대입으로 각 값을 설치한 뒤 남은 본문을 보존해 서로 재귀적인 helper가 하나의 지역 환경을 공유하게 합니다.

생각해 볼 질문

평가기는 왜 프로시저 값을 하나라도 설치하기 전에 내부 바인딩을 모두 먼저 만들어야 할까요?

  • 프로시저 본문 앞쪽의 내부 정의 알아보기
  • 프로시저 정의 축약형을 명시적인 lambda 값으로 바꾸기
  • 대입 전에 모든 지역 이름을 unassigned 표식으로 만들기
  • 대입 뒤에 정의가 아닌 원래 본문 순서 보존하기
  • 원래 프로그램과 변환한 프로그램을 같은 유한 입력으로 비교하기
  • 관찰한 일치와 모든 프로그램·구현에 대한 증명을 구분하기

scan-out 단계는 프로시저 본문을 데이터로 읽습니다. 앞쪽 define 형식을 모으고 프로시저 정의 축약형을 lambda 식으로 바꾸며, 각 이름을 명시적인 unassigned 표식으로 갖는 let 바인딩을 먼저 만듭니다. 그다음 각 정의의 set!을 놓고 마지막에 원래 남은 본문을 붙입니다. 따라서 어느 클로저가 적용되기 전에 이름들이 한 환경 안에 함께 존재해 서로 재귀적인 helper가 같은 지역 환경을 공유합니다.

두 번째 프로그램은 두 형태를 실제로 실행합니다. classify-original은 내부 정의를 그대로 사용하고 classify-scanned는 변환된 let과 set! 구조를 명시적으로 씁니다. 7과 8에서 같은 결과가 나오며 equal?은 참을 보고합니다. 이 유한 비교는 이번 변환과 입력을 확인할 뿐 모든 Scheme 프로그램의 의미 동등성을 증명하거나 숨은 리스펙스 컴파일 단계를 드러내거나 모든 구현의 중간 표현을 규정하지 않습니다.

리스펙스 · SICP 코드SICP에 필요한 Scheme 호환 문법을 리스펙스 SICP 프로필로 실행합니다.
(begin
  (define body
    '((define (even-step value)
        (if (= value 0)
            #t
            (odd-step (- value 1))))
      (define (odd-step value)
        (if (= value 0)
            #f
            (even-step (- value 1))))
      (list (even-step n) (odd-step n))))
  (define (definition? expression)
    (and (pair? expression)
         (eq? (car expression) 'define)))
  (define (take-definitions expressions)
    (if (and (pair? expressions)
             (definition? (car expressions)))
        (cons (car expressions)
              (take-definitions (cdr expressions)))
        '()))
  (define (drop-definitions expressions)
    (if (and (pair? expressions)
             (definition? (car expressions)))
        (drop-definitions (cdr expressions))
        expressions))
  (define (definition-name definition)
    (if (symbol? (cadr definition))
        (cadr definition)
        (car (cadr definition))))
  (define (definition-value definition)
    (if (symbol? (cadr definition))
        (caddr definition)
        (cons 'lambda
              (cons (cdr (cadr definition))
                    (cddr definition)))))
  (define (make-binding definition)
    (list (definition-name definition)
          (list 'quote '*unassigned*)))
  (define (make-assignment definition)
    (list 'set!
          (definition-name definition)
          (definition-value definition)))
  (define (scan-out-defines expressions)
    (let ((definitions (take-definitions expressions))
          (remaining (drop-definitions expressions)))
      (if (null? definitions)
          expressions
          (list
            (cons 'let
                  (cons (map make-binding definitions)
                        (append (map make-assignment definitions)
                                remaining)))))))
  (car (scan-out-defines body)))
리스펙스 학습용 런타임리스펙스 SICP 프로필 1.0.0
리스펙스 SICP 런타임 불러오는 중
리스펙스 · SICP 코드UTF-8 1,830 / 1,048,576바이트
예제
결과
출력
진단
보이는 실행 흐름0 / 0 개의 실행 이벤트
    이 브라우저 결과는 리스펙스 바우치나 권한이 아닙니다.wasm —
    예상 관찰

    변환 프로그램은 even-step과 odd-step을 인용된 *unassigned* 표식에 바인딩한 let 식 하나와 두 set! 형식, 원래 list 본문을 반환합니다. 비교 프로그램은 (#t ((#f #t) (#t #f)) ((#f #t) (#t #f)))를 반환합니다.

    실행 흐름에서 볼 점

    변환 실행에서는 인용된 소스 데이터를 읽는 일과 앞쪽 정의 모으기, 바인딩 만들기, 프로시저 정의를 lambda 값으로 바꾸기, 대입 만들기, 남은 본문 붙이기를 구분하세요. 비교 실행에서는 하나의 let 환경과 두 set!, 번갈아 호출되는 클로저를 찾으세요. 일치는 선택한 입력에 대해서만 보고됩니다.

    직접 해보기

    힌트를 보기 전에 프로그램을 바꿔 보세요.

    세 개의 서로 재귀적인 나머지 프로시저로 3의 배수를 알아보는 내부 helper를 추가하세요. 원래 버전과 명시적으로 scan-out한 버전을 모두 쓰고 8과 9의 결과를 비교하세요.

    힌트 하나 보기

    대입 전에 이름 세 개를 모두 만드세요. 각 클로저는 1을 빼고 다음 나머지 프로시저를 호출하며 나머지 0 프로시저만 0에서 참을 반환합니다.