지연 모듈 임포트: lazy-require

지연 모듈 임포트: lazy-require

lazy-require는 함수가 실제로 호출될 때까지 모듈 로딩을 미루는 매크로예요. 무거운 모듈의 로딩 비용을 필요한 순간까지 지연시키고 싶을 때 씁니다.

출처: Racket Reference

본문

(require racket/lazy-require)    ; package: base

이 절에서 문서화하는 바인딩은 racket/baseracket이 아니라 racket/lazy-require 라이브러리가 제공합니다.

lazy-require

syntax

(lazy-require [module-path (fun-import ...)] ...)

fun-import = fun-id
           | (orig-fun-id fun-id)

fun-id를 함수로 정의하는데, 이 함수는 호출되면 module-path가 지정한 모듈에서 orig-fun-id라는 이름의 export를 동적으로 require하고 같은 인자들로 호출합니다. orig-fun-id가 주어지지 않으면 기본값은 fun-id입니다.

둘러싼 상대 phase level이 0이 아니면, module-path는 submodule에도 배치됩니다(그 submodule 안의 phase level 0에서 define-runtime-module-path-index의 사용과 함께). 도입되는 submodule들은 lazy-require-aux_n-m라는 이름을 갖는데, 여기서 n은 phase level 숫자이고 m은 숫자입니다.

지연 require된 함수의 사용이 모듈 로딩을 촉발하면, 그것은 (함수가 모듈 컴파일 과정에서 사용되는 경우를 대비해) 간접 컴파일 의존성을 선언하는 register-external-module의 사용도 촉발합니다.

예시:

> (lazy-require
   [racket/list (partition)])
> (partition even? '(1 2 3 4 5))
'(2 4)
'(1 3 5)

> (module hello racket/base
   (provide hello)
   (printf "starting hello server\n")
   (define (hello) (printf "hello!\n")))
> (lazy-require
   ['hello ([hello greet])])
> (greet)
starting hello server
hello!

lazy-require-syntax

syntax

(lazy-require-syntax [module-path (macro-import ...)] ...)

macro-import = macro-id
             | (orig-macro-id macro-id)

lazy-require와 같지만 매크로용입니다. 즉, 각 macro-id를 매크로로 정의하는데, 이 매크로는 사용되면 주어진 module-path에서 매크로의 구현을 동적으로 로드합니다. orig-macro-id가 주어지지 않으면 기본값은 macro-id입니다.

크고 복잡한 매크로를 가진 라이브러리의 구현에서 lazy-require-syntax를 사용해서, 라이브러리 클라이언트가 매크로의 "컴파일러"에 의존하지 않게 하세요. 매크로의 컴파일-타임 컴포넌트가 예외적으로 큰 경우(Typed Racket처럼 타입 체커와 최적화기를 포함하는 경우)만 lazy-require-syntax의 혜택을 받는다는 점에 유의하세요. 일반적인 매크로는 그렇지 않습니다.

경고: lazy-require-syntax는 Racket의 모듈 로더와 링커가 의존하는 불변식(invariants)을 깨뜨립니다. 이 불변식들은 보통 매크로가 만들어낸 코드의 참조들이 코드가 실행되기 전에 로드되도록 보장합니다. lazy-require-syntax를 안전하게 쓰려면 매크로 구현에 특정한 구조가 필요합니다. (특히 lazy-require-syntax는 클라이언트 코드에서 단순히 도입될 수 없습니다.) 매크로 구현은 다음 규칙을 따라야 합니다:

  • interface 모듈은 runtime-support 모듈을 require해야 한다.
  • compiler 모듈은 상대 경로가 아닌 절대 모듈 경로를 통해 runtime-support 모듈을 require해야 한다.

"interface, compiler, runtime-support 모듈"의 개념을 설명하기 위해, 매크로를 export하는 예시 모듈을 보겠습니다:

(module original racket/base
  (define (ntimes-proc n thunk)
    (for ([i (in-range n)]) (thunk)))
  (define-syntax-rule (ntimes n expr)
    (ntimes-proc n (lambda () expr)))
  (provide ntimes))

lazy-require-syntax를 사용해 ntimes 매크로 트랜스포머의 구현을 지연 로드한다고 가정해 봅시다. 원래 모듈은 세 부분으로 나누어야 합니다:

(module runtime-support racket/base
  (define (ntimes-proc n thunk)
    (for ([i (in-range n)]) (thunk)))
  (provide ntimes-proc))

(module compiler racket/base
  (require 'runtime-support)
  (define-syntax-rule (ntimes n expr)
    (ntimes-proc n (lambda () expr)))
  (provide ntimes))

(module interface racket/base
  (require racket/lazy-require)
  (require 'runtime-support)
  (lazy-require-syntax ['compiler (ntimes)])
  (provide ntimes))

runtime support 모듈은 매크로가 참조하는 함수와 값 정의를 담고 있습니다. compiler 모듈은 매크로 정의 자체—컴파일 타임 뒤에 "사라지는" 코드 부분—를 담고 있습니다. interface 모듈은 매크로 트랜스포머를 지연 로드하지만, runtime support 모듈을 정상적으로 require해서 런타임에 정의되도록 보장합니다. 더 큰 예시에서는 물론 runtime support와 compiler가 각각 여러 모듈로 구성될 수 있습니다.

다음은 runtime support를 별도 모듈로 분리하지 않았을 때 무슨 일이 일어나는지 보여주는 경우입니다:

> (module bad-no-runtime racket/base
   (define (ntimes-proc n thunk)
     (for ([i (in-range n)]) (thunk)))
   (define-syntax-rule (ntimes n expr)
     (ntimes-proc n (lambda () expr)))
   (provide ntimes))
> (module bad-client racket/base
   (require racket/lazy-require)
   (lazy-require-syntax ['bad-no-runtime (ntimes)])
   (ntimes 3 (printf "hello?\n")))
> (require 'bad-client)
require: namespace mismatch;
 reference to a module that is not instantiated
 module: 'bad-no-runtime
 phase: 0

interface 모듈이 runtime support 모듈에 대한 의존성을 도입하지 않을 때도 비슷한 오류가 발생합니다.

더 알아보기