모듈: module, module*, …

모듈: module, module*, …

Racket에서 모듈은 프로그램을 조직하는 기본 단위예요. module 형식으로 최상위 모듈이나 서브모듈을 선언하고, module*·module+로 서브모듈을 다루며, #%module-begin 계열이 렌더링 단계를 담당합니다.

출처: Racket Reference - Modules: module, module*, ...

본문

문법

(module id module-path form ...)

최상위 모듈이나 서브모듈을 선언해요. 최상위 모듈의 경우 current-module-declare-name 파라미터가 설정되어 있으면 그 파라미터 값을 모듈 이름으로 사용하고 id는 무시하며, 그렇지 않으면 (quote id)가 선언된 모듈의 이름이 됩니다. 서브모듈의 경우 idsubmod 모듈 경로 안의 요소로 쓰일 서브모듈의 이름이에요. module 형식은 표현식 맥락이나 내부 정의 맥락에서는 허용되지 않습니다.

문법

(module id module-path form ...)

module-path 형식은 require에서와 같아야 하며, 본문 form들의 초기 바인딩을 공급해요. 즉 form들 앞에 (require module-path) 접두사가 붙은 것처럼 취급되는데, 다만 module-path가 도입한 바인딩은 모듈 본문 form의 정의와 require가 가릴(shadow) 수 있습니다.

단일 form이 제공되면 모듈 시작(module-begin) 맥락에서 부분 확장됩니다. 확장이 #%plain-module-begin으로 이어지면 #%plain-module-begin의 본문이 모듈의 본문이 돼요. 부분 확장이 다른 어떤 원시 형식으로 이어지면, 그 형식은 모듈 본문의 렉시컬 맥락을 사용해 #%module-begin으로 감싸집니다. 이 식별자는 초기 module-path import가 바인딩해야 하며, 그 확장이 모듈 본문을 공급하는 #%plain-module-begin을 만들어야 해요. 부분 확장이 compiled-module-expression?의 의미로 컴파일된 모듈을 만들어 내면, 그 컴파일된 모듈이 바깥 모듈용으로 사용되는데(모든 다른 확장·컴파일 단계를 건너뜀), 그러나 그런 결과는 syntax-local-compiling-module?가 참을 만드는 컴파일 모드에서, 그리고 현재 코드 검사자가 초기 검사자일 때만 허용됩니다. 마지막으로 여러 form이 제공되면 단일 form#%plain-module-begin으로 확장되지 않는 경우처럼, #%module-begin으로 감싸져요.

이런 감싸기(있으면)가 있은 뒤, 어떤 확장 전에 'enclosing-module-name 프로퍼티가 #%module-begin 문법 객체에 붙습니다(Syntax Object Properties 참조). 그 프로퍼티의 값은 id에 대응하는 심볼이에요.

form은 모듈 맥락에서 부분 확장됩니다(Partial Expansion 참조). 이후의 동작은 형식의 모양에 따라 달라져요.

  • begin 형식이면 하위 형식들이 모듈 본문으로 펼쳐지고 begin 대신 그 자리에서 즉시 처리됩니다.
  • define-syntaxes 형식이면 오른쪽이(페이즈 1에서) 평가되고, 바인딩이 즉시 설치되어 모듈 안에서 추가 부분 확장에 쓰여요. 오른쪽 평가는 let-syntax에서처럼 current-namespace를 설정하도록 parameterize됩니다.
  • begin-for-syntax 형식이면 본문이(페이즈 1에서) 확장되고 평가됩니다. begin-for-syntax 형식 안의 확장은 module 본문과 같은 부분 확장 과정으로 진행되지만, 더 높은 페이즈에서 진행되고 모든 페이즈의 모든 #%provide 형식을 module 확장의 끝까지 저장해 둡니다. 본문 평가는 let-syntax에서처럼 current-namespace를 설정하도록 parameterize됩니다.
  • 형식이 #%require 형식이면 바인딩이 즉시 도입되고, 가져온 모듈이 적절하게 인스턴스화되거나 방문됩니다.
  • 형식이 #%provide 형식이면 본문의 나머지 이후에 처리하도록 기록됩니다.
  • 형식이 define-values 형식이면 바인딩이 즉시 설치되지만, 오른쪽 표현식은 더 확장되지 않습니다.
  • 형식이 module 형식이면 즉시 확장되고, 현재 최상위 바깥 모듈의 확장 범위 동안 선언됩니다.
  • 형식이 module* 형식이면 더 확장되지 않습니다.
  • 마찬가지로 형식이 표현식이면 더 확장되지 않습니다.

모든 form이 이렇게 부분 확장된 뒤, 남은 표현식 형식들(정의의 오른쪽 포함)이 표현식 맥락에서 확장됩니다. 모든 표현식 형식 다음에, #%provide 형식들이(페이즈 무관하게) 확장된 모듈에 나타난 순서대로 처리됩니다. 마지막으로 모든 module* 형식이 순서대로 확장되어, 각각이 이후의 module* 형식이 사용할 수 있게 됩니다. 바깥 모듈 자신도 module* 서브모듈이 사용할 수 있어요.

모든 가져온 식별자의 스코프는 중첩된 modulemodule* 형식(후자의 경우 module-path#f가 아닐 때)을 제외한 모듈 본문 전체를 덮습니다. 모듈 본문 안에서 정의된 어떤 식별자의 스코프도 마찬가지로 그런 중첩 module·module* 형식을 제외한 모듈 본문 전체를 덮어요. 문법 정의의 순서는 문법 이름의 스코프에 영향을 주지 않습니다. A의 변환기가 B를 담은 표현식을 만들 수 있고, B의 변환기가 A를 담은 표현식을 만들 수 있죠. AB의 선언 순서와 무관합니다. 다만 문법 정의를 만들어 내는 문법 형식은 사용되기 전에 정의되어야 해요.

어떤 식별자도 단일 모듈 안의 어떤 페이즈 수준에서 한 번 이상 가져오거나 정의할 수 없습니다. 단 define-valuesdefine-syntaxes를 통한 정의는 #%require를 통한 import를 가릴 수 있어요 — 앞선 #%declare 형식이 #:require=define을 포함하지 않는 한. 모든 내보낸 식별자는 가져오거나 정의되어야 합니다. 어떤 표현식도 최상위 변수를 참조할 수 없습니다. 바깥 모듈의 바인딩이 보이지 않는(module-path 대신 #f를 가진 중첩 module*) module* 형식은 바깥 모듈의 바인딩을 가리는 바인딩을 정의하거나 가져올 수 있습니다.

module 형식의 평가는 모듈 본문의 표현식을 평가하지 않아요(가끔 재선언의 경우는 제외; Module Redeclarations 참조). 평가는 그저 모듈을 선언할 뿐이며, 그 전체 이름은 id 또는 (current-module-declare-name)에 달려 있습니다.

모듈 본문은 모듈이 requiredynamic-require로 명시적으로 인스턴스화될 때만 실행됩니다. 호출 시, 가져온 모듈들은 그 모듈로 require된 순서대로 인스턴스화돼요(다만 더 이른 인스턴스화나 전이적 require가 주어진 모듈 안에서 모듈을 자기 순서보다 앞서 인스턴스화를 촉발할 수는 있습니다). 그런 다음 표현식과 정의가 모듈 안에 나타난 순서대로 평가됩니다. 각 표현식·정의 평가는 기본 프롬프트 태그에 대해 그리고 자기 인자를 다음 바깥 프롬프트로 다시 중단(abort)하고 전파하는 프롬프트 핸들러를 사용해 연속 프롬프트로 감싸집니다(call-with-continuation-prompt 참조). 각 정의 평가 뒤에는 프롬프트 바깥에서 그 정의의 각 변수에 값이 있는지 검사가 따릅니다. 값을 설치하는 프롬프트로 제한된 연속 부분을 건너뛰면 exn:fail:contract:variable? 예외가 발생해요.

모듈 본문의 더 높은 페이즈 수준 부분은 런타임 부분과 비슷하게 구분됩니다. 예를 들어 begin-for-syntax 안의 모듈 부분은 모듈이 확장될 때와 방문될 때 모두 연속 프롬프트로 구분됩니다. define-syntaxes 형식의 평가는 구분되지만, define-values와 달리 문법 정의가 완료됐는지 검사는 없습니다.

정의되기 전에 모듈 수준 변수에 접근하면 정의되지 않은 전역 변수에 접근하는 것처럼 런타임 오류가 신호됩니다. 모듈(완전 확장된 형태)이 모듈 안에 정의된 식별자에 대한 set!을 담지 않으면, 그 식별자는 정의된 뒤 *상수(constant)*가 돼요. 그 값을 나중에 바꿀 수 없으며, 반영적 메커니즘을 통해서도 바꿀 수 없습니다. 다만 compile-enforce-module-constants 파라미터로 상수 시행을 끌 수 있습니다.

module 형식을 나타내는 문법 객체에 'module-language 문법 프로퍼티가 붙어 있고, 그 프로퍼티 값이 세 요소의 벡터인데 첫 요소가(module-path?의 의미로) 모듈 경로이고 두 번째가 심볼이면, 그 프로퍼티 값은 대응하는 컴파일된/선언된 모듈에 보존됩니다. 벡터의 세 번째 성분은 인쇄·read 가능해야 해서 마샬된 바이트코드에 보존될 수 있어야 해요. racket/baseracket 언어는 '#(racket/language-info get-info #f)module 형식에 붙입니다. module-compiled-language-info, module->language-info, racket/language-info도 함께 보세요.

Modules and Module-Level Variables, Module Expansion, Phases, and Visits, Information on Expanded Modules도 함께 보세요.

예시:

(module duck racket/base
  (provide num-eggs quack)
  (define num-eggs 2)
  (define (quack n)
    (unless (zero? n)
      (printf "quack\n")
      (quack (sub1 n)))))

버전 6.3 (패키지 base)에서 변경: define-syntaxesdefine-values가 이전의 어떤 import도 가리도록 변경되고, 중첩 module·module* 형식의 'submodule 문법 프로퍼티 값 사용이 제거됨.

문법

(module* id module-path form ...)

(module* id #f form ...)

Racket Guide의 Submodules가 module*를 소개합니다.

module과 같지만, 모듈 안의 서브모듈을 선언할 때만 쓰이고 바깥 모듈을 require할 수 있는 서브모듈을 위한 것입니다.

id 뒤의 module-path 대신 #f는 바깥 모듈의 모든 바인딩이 서브모듈에 보인다는 뜻이에요. 그 경우 module* 형식을 감싸는 begin-for-syntax 형식은 서브모듈에 대한 바깥 모듈 바인딩의 페이즈 수준을 바꿉니다. 매크로 확장기는 module* 형식의 페이즈 수준을 바꿔 그 본문이 페이즈 0에서 시작하도록 하고, 확장한 다음 페이즈 수준 이동을 되돌리는 방식으로 이런 중첩을 처리합니다. 이 과정이 문법 객체를 'origin 문법 프로퍼티 값으로 남겨 확장된 모듈과 동기화가 어긋나게 할 수 있음을 주의하세요.

module* 형식이 module-path를 가지면, 서브모듈 확장은 module 형식과 똑같이 바깥 모듈의 스코프를 제거하면서 시작됩니다. 서브모듈을 감쌀 수 있는 begin-for-syntax 형식을 보상하는 이동은 없어요.

문법

(module+ id form ...)

Racket Guide의 Main and Test Submodules가 module+를 소개합니다.

id라는 이름의 서브모듈을 선언하고/하거나 추가해요.

id에 대한 각 추가는 순서대로 합쳐져 바깥 모듈의 끝에서 (module* id #f ....)를 사용해 전체 서브모듈을 이룹니다. 주어진 id에 대한 module+가 하나뿐이면 (module+ id form ...)(module* id #f form ...)과 동등하지만, 여전히 바깥 모듈의 끝으로 옮겨집니다.

'origin-form-srcloc를 가진 module* 형식의 문법 프로퍼티가 기여하는 각 module+ 형식의 srcloc를 기록해요.

모듈이 module+로 선언된 여러 서브모듈을 담으면, 각 서브모듈의 초기 module+ 선언들의 상대적 순서가 바깥 모듈 끝의 module* 선언들의 상대적 순서를 결정합니다.

서브모듈은 module+module 또는 module*를 함께 써서 정의하면 안 됩니다. 즉 서브모듈이 module+ 조각으로 만들어졌다면, 반드시 module+ 조각으로만 만들어져야 해요.

버전 8.9.0.1 (패키지 base)에서 변경: 'origin-form-srcloc 문법 프로퍼티 추가됨.

문법

(#%module-begin form ...)

모듈 시작 맥락에서만 유효하며, modulemodule* 형식이 처리합니다.

racket/base#%module-begin 형식은 current-print가 결정하는 프린트 핸들러를 사용해 #<void>가 아닌 결과를 인쇄하도록 모든 최상위 표현식을 감싸고, 인쇄한 뒤 그 값들을 반환하기도 해요. 이 인쇄는 #%module-begin 확장의 일부로 추가되므로, module 자신이 추가하는 프롬프트는 인쇄 래퍼 바깥에 있습니다. 그리고 이는 인쇄 후 반환되는 값들을 유의미하게 만들 수 있는데, 연속을 캡처해 다른 맥락에서 호출할 수 있기 때문이에요.

racket/base#%module-begin 형식은 또한(어떤 다른 form보다 먼저) configure-runtime 서브모듈을 선언하는데, 어떤 formconfigure-runtime이라는 이름의 즉시 module 또는 module* 형식이 아닌 한 그래요. configure-runtime 서브모듈이 추가되면, 그 서브모듈은 racket/runtime-configconfigure 함수를 호출합니다.

문법

(#%printing-module-begin form ...)

모듈 시작 맥락에서만 유효합니다.

#%module-begin과 같지만, configure-runtime 서브모듈을 추가하지 않아요.

문법

(#%plain-module-begin form ...)

모듈 시작 맥락에서만 유효하며, modulemodule* 형식이 처리합니다.

문법

(#%declare declaration-keyword ...)

declaration-keyword = #:cross-phase-persistent | #:empty-namespace | #:require=define | #:flatten-requires | #:unlimited-compile | #:unsafe | #:realm identifier

모듈의 런타임 또는 반영적 속성에 영향을 주는 선언들:

  • #:cross-phase-persistent — 모듈을 교차 페이즈 영속(cross-phase persistent)으로 선언하고, 모듈이 교차 페이즈 영속 모듈의 제약을 충족하지 않으면 문법 오류를 보고해요.
  • #:empty-namespace — 이 모듈에 대한 module->namespace가 바인딩이 없는 네임스페이스를 만들어 내도록 선언해요. 이렇게 네임스페이스 지원을 제한하면 그렇지 않았다면 모듈을 위해 보존해야 했을 렉시컬 정보를 줄일 수 있습니다.
  • #:require=define — 모듈 본문 안에서 그 직후의 어떤 정의도 #%require(또는 require) 바인딩을 가리지 못하도록 선언해요. 이 선언은 모듈의 초기 import(즉 모듈의 언어)의 셰도잉에는 영향을 주지 않습니다.
  • #:flatten-requires — 모듈의 컴파일된 형태가 전이적 import를 하나의 평탄화된 리스트로 모아야 한다는 성능 힌트를 선언해요. 이는 모듈이 인스턴스화되거나 namespace-attach-module·namespace-attach-module-declaration으로 붙을 때 성능을 개선할 수 있습니다. 다만 import 평탄화는 서로를 사용하고 전이적 import 하위 트리가 겹치는 여러 모듈에 적용되면 역효과를 낼 수 있어요.
  • #:unlimited-compile — 특히 큰 모듈 본문에 대해 컴파일이 해석(interpreted) 모드로 폴백하지 않도록 선언해요. 그렇지 않으면 모듈 본문의 크기(linklet으로 변환된)와 PLT_CS_COMPILE_LIMIT 환경 변수(CS Compilation Modes 참조)에 따라 컴파일 모드가 선택됩니다.
  • #:unsafe — 모듈이 exn:fail:contract를 촉발할 수 있는 검사 없이 컴파일될 수 있다고 선언하고, exn:fail:contract가 발생했어야 하는 평가에서의 결과 동작은 명세되지 않아요(Unsafe Operations 참조). 예를 들어 car 사용은 unsafe-car 사용으로 컴파일될 수 있고, unsafe-car를 페어가 아닌 것에 적용하면 동작이 명세되지 않습니다. #:unsafe 선언 키워드는 현재 코드 검사자가 초기 검사자일 때만 허용됩니다. 매크로는 확장 맥락에 따라 (variable-reference-from-unsafe? (#%variable-reference)) 사용으로 확장해 조건부로 안전하지 않은 코드를 만들어 낼 수 있습니다.
  • #:realm identifier — 모듈과 모듈 안의 어떤 프로시저에게 identifier의 심볼 형태인 영역(realm)을 부여한다고 선언하며, 효과적으로 current-compile-realm의 값을 덮어씁니다.

#%declare 형식은 모듈 맥락이나 모듈 시작 맥락에 나타나야 합니다. 각 declaration-keywordmodule 본문 안에서 최대 한 번 선언할 수 있습니다.

버전 6.3 (패키지 base)에서 변경: #:empty-namespace 추가됨.

버전 7.9.0.5에서 변경: #:unsafe 추가됨.

버전 8.4.0.2에서 변경: #:realm 추가됨.

버전 8.6.0.9에서 변경: #:require=define 추가됨.

버전 8.13.0.4에서 변경: #:flatten-requires 추가됨.

버전 8.13.0.9에서 변경: #:unlimited-compile 추가됨.

더 알아보기