지연 모듈 임포트: lazy-require
지연 모듈 임포트: lazy-require
lazy-require는 함수가 실제로 호출될 때까지 모듈 로딩을 미루는 매크로예요. 무거운 모듈의 로딩 비용을 필요한 순간까지 지연시키고 싶을 때 씁니다.
출처: Racket Reference
본문
(require racket/lazy-require) ; package: base
이 절에서 문서화하는 바인딩은 racket/base나 racket이 아니라 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 모듈에 대한 의존성을 도입하지 않을 때도 비슷한 오류가 발생합니다.