모듈
모듈 (Modules)
Nim은 모듈(module) 개념으로 프로그램을 여러 조각으로 나눌 수 있게 해줘요. 각 모듈은 자기만의 파일에 있어야 하고, 자기만의 네임스페이스(namespace) 를 가져요. 모듈은 정보 은닉(information hiding)과 별도 컴파일(separate compilation)을 가능하게 만드는 장치예요.
모듈은 import 문으로 다른 모듈의 심볼에 접근할 수 있어요. 순환 모듈 의존성(recursive module dependencies)은 허용되지만, 다루기가 조금 미묘해요. 별표(*)로 표시된 최상위 심볼만 내보내져요(export). 유효한 모듈 이름은 유효한 Nim 식별자여야 해요(그래서 그 파일명은 identifier.nim이 돼요).
모듈을 컴파일하는 알고리즘은 이래요:
- 평소처럼 모듈 전체를 컴파일하되, import 문을 재귀적으로 따라가요.
- 순환(cycle)이 생기면 이미 파싱된 심볼(내보내진 것)만 import해요. 알 수 없는 식별자가 나오면 중단해요.
이건 예를 보는 게 가장 명확해요.
# 모듈 A
type
T1* = int # 모듈 A는 타입 `T1`을 내보냄
import B # 컴파일러가 B를 파싱하기 시작함
proc main() =
var i = p(3) # 여기선 B가 완전히 파싱됐으므로 동작함
main()
# 모듈 B
import A # A는 여기서 파싱되지 않음! A의 이미 알려진 심볼만 import됨.
proc p*(x: A.T1): A.T1 =
# T1이 이미 A의 인터페이스 심볼 테이블에
# 추가돼 있으므로 동작함
result = x + 1
Import 문 (Import statement)
import 키워드 뒤에는 모듈 이름 목록이 올 수도 있고, 단일 모듈 이름 뒤에 except 목록이 와서 일부 심볼이 import되지 않게 막을 수도 있어요.
import std/strutils except `%`, toUpperAscii
# 그러면 이건 동작하지 않음:
echo "$1" % "abc".toUpperAscii
except 목록의 항목이 정말 그 모듈에서 내보내지는지는 검사하지 않아요. 덕분에 어떤 버전이 그 식별자를 내보내지 않더라도, 모듈의 다른 버전들을 대상으로 컴파일할 수 있어요.
import 문은 최상위 레벨에서만 쓸 수 있어요.
import/include 문에는 문자열 리터럴을 쓸 수도 있어요. 사용하면 컴파일러가 경로 치환을 수행해요.
Include 문 (Include statement)
include 문은 모듈을 import하는 것과는 근본적으로 다른 일을 해요. 그저 파일의 내용을 그대로 포함시킬 뿐이에요. include 문은 큰 모듈을 여러 파일로 쪼갤 때 유용해요.
include fileA, fileB, fileC
include 문은 최상위 레벨 밖에서도 쓸 수 있어요. 이렇게요.
# 모듈 A
echo "Hello World!"
# 모듈 B
proc main() =
include A
main() # => Hello World!
Import에서의 모듈 이름 (Module names in imports)
as 키워드로 모듈 별칭(alias)을 도입할 수 있어요. 별칭을 붙이면 원래 모듈 이름은 더 이상 쓸 수 없어요.
import std/strutils as su, std/sequtils as qu
echo su.format("$1", "lalelu")
path/to/module 또는 "path/to/module" 표기로 하위 디렉토리에 있는 모듈을 가리킬 수 있어요.
import lib/pure/os, "lib/pure/times"
모듈 이름은 여전히 strutils이지 lib/pure/strutils가 아니라는 점에 주의하세요. 그래서 이렇게는 할 수 없어요.
import lib/pure/strutils
echo lib/pure/strutils.toUpperAscii("abc")
마찬가지로 이름이 이미 strutils이므로, 아래 코드는 의미가 없어요.
import lib/pure/strutils as strutils
디렉토리에서의 집합 import (Collective imports from a directory)
import dir / [moduleA, moduleB] 문법으로 같은 디렉토리에 있는 여러 모듈을 한 번에 import할 수 있어요.
경로 이름은 문법적으로 Nim 식별자거나 문자열 리터럴이에요. 경로 이름이 유효한 Nim 식별자가 아니면 문자열 리터럴로 써야 해요.
import "gfx/3d/somemodule" # '3d'가 유효한 Nim 식별자가 아니므로 따옴표로 감쌈
의사 import/include 경로 (Pseudo import/include paths)
디렉토리는 이른바 "의사 디렉토리(pseudo directory)"가 될 수도 있어요. 같은 경로를 가진 모듈이 여럿 있을 때 모호함을 피하는 데 쓰여요.
의사 디렉토리는 두 가지가 있어요:
std:std의사 디렉토리는 Nim 표준 라이브러리의 추상적인 위치예요. 예를 들어import std / strutils문법은 표준 라이브러리의strutils모듈을 모호함 없이 가리킬 때 써요.pkg:pkg의사 디렉토리는 Nimble 패키지를 모호함 없이 가리킬 때 써요. 다만 이 문서의 범위 밖인 기술적 세부사항을 제외하면, 그 의미는 검색 경로에서 모듈 이름을 찾되 표준 라이브러리 위치는 무시한다는 뜻이에요. 다시 말해std의 반대예요.
모든 표준 라이브러리 모듈 import가 import 이름의 일부로 std/ "의사 디렉토리"를 포함하는 것이 권장·선호되지만, 현재로서는 강제되지는 않아요.
from import 문 (From import statement)
from 키워드 뒤에 모듈 이름이 오고, 그다음에 명시적인 완전 자격(full qualification) 없이 쓰고 싶은 심볼들을 나열하는 import가 이어져요.
from std/strutils import `%`
echo "$1" % "abc"
# 언제나 가능한 완전 자격:
echo strutils.replace("abc", "a", "z")
모듈을 import하되, 그 module 안의 모든 심볼에 완전 자격 접근을 강제하고 싶다면 from module import nil을 쓸 수도 있어요.
Export 문 (Export statement)
export 문은 심볼 전달(symbol forwarding)에 쓰여요. 그래서 클라이언트 모듈이 어떤 모듈의 의존성을 굳이 import하지 않아도 되게 해요.
# 모듈 B
type MyObject* = object
# 모듈 A
import B
export B.MyObject
proc `$`*(x: MyObject): string = "my object"
# 모듈 C
import A
# B.MyObject가 여기서 암묵적으로 import됨:
var x: MyObject
echo $x
내보내는 심볼이 다른 모듈이면, 그 모듈의 모든 정의가 전달돼요. except 목록으로 일부 심볼을 제외할 수도 있어요.
이때 주의할 점: export할 때는 모듈 이름만 지정하면 돼요.
import foo/bar/baz
export baz
스코프 규칙 (Scope rules)
식별자는 선언된 지점부터 그 선언이 일어난 블록의 끝까지 유효해요. 식별자가 알려져 있는 범위를 그 식별자의 스코프(scope)라고 해요. 식별자의 정확한 스코프는 선언된 방식에 따라 달라져요.
블록 스코프 (Block scope)
블록의 선언 부분에서 선언된 변수의 스코프는 선언 지점부터 블록의 끝까지 유효해요. 블록이 두 번째 블록을 포함하고 있고 그 안에서 식별자가 다시 선언되면, 그 내부 블록 안에서는 두 번째 선언이 유효해요. 내부 블록을 벗어나면 첫 번째 선언이 다시 유효해져요. 식별자는 같은 블록 안에서 다시 정의할 수 없어요. 다만 프로시저나 이터레이터 오버로딩을 위한 경우는 예외예요.
튜플 또는 객체 스코프 (Tuple or object scope)
튜플이나 객체 정의 안의 필드 식별자는 다음 위치에서 유효해요:
- 튜플/객체 정의의 끝까지.
- 주어진 튜플/객체 타입 변수의 필드 지정자(designator).
- 그 객체 타입의 모든 파생 타입에서.
모듈 스코프 (Module scope)
모듈의 모든 식별자는 선언 지점부터 모듈의 끝까지 유효해요. 간접적으로 의존하는 모듈의 식별자는 쓸 수 없어요. system 모듈은 모든 모듈에 자동으로 import돼요.
모듈이 서로 다른 두 모듈에서 같은 식별자를 import하면, 그 식별자는 모호한(ambiguous) 것으로 간주돼요. 이렇게 해결할 수 있어요:
module.identifier로 식별자를 자격(qualify) 지어 모듈 간 모호함을 해소해요. (모듈 이름 자체가 모호한 경우는 아래를 보세요.)- 식별자를 루틴으로 호출하면 오버로드 해석(overload resolution)이 일어나는데, 한 오버로드가 다른 것보다 강하게 일치하면 모호함이 풀려요.
- 컴파일러가 식별자의 타입을 추론할 수 있는 문맥에서 쓰면, 한 정의가 다른 것보다 그 타입에 강하게 일치할 때 모호함이 풀려요.
# 모듈 A
var x*: string
proc foo*(a: string) =
echo "A: ", a
# 모듈 B
var x*: int
proc foo*(b: int) =
echo "B: ", b
# 모듈 C
import A, B
foo("abc") # A: abc
foo(123) # B: 123
let inferred: proc (x: string) = foo
foo("def") # A: def
write(stdout, x) # error: x is ambiguous
write(stdout, A.x) # no error: qualifier used
proc bar(a: int): int = a + 1
assert bar(x) == x + 1 # no error: only A.x of type int matches
var x = 4
write(stdout, x) # not ambiguous: uses the module C's x
모듈은 이름을 공유할 수 있어요. 하지만 모듈 이름으로 식별자를 자격 지으려 하면 컴파일러가 모호한 식별자(ambiguous identifier) 오류로 실패해요. 모듈을 별칭 지으면 식별자를 자격 지을 수 있어요.
# 모듈 A/C
proc fb* = echo "fizz"
# 모듈 B/C
proc fb* = echo "buzz"
import A/C
import B/C
C.fb() # Error: ambiguous identifier: 'C'
import A/C as fizz
import B/C
fizz.fb() # Works
패키지 (Packages)
파일 트리 안의 모듈 모음으로, 그 트리의 루트에 identifier.nimble 파일이 있으면 그것을 Nimble 패키지라고 불러요. 유효한 패키지 이름은 유효한 Nim 식별자여야 하며, 그래서 그 파일명은 identifier.nimble이 돼요. 여기서 identifier가 원하는 패키지 이름이에요. .nimble 파일이 없는 모듈에는 unknown이라는 패키지 식별자가 부여돼요.
패키지의 구분 덕분에 진단용 컴파일러 메시지를 현재 프로젝트의 패키지와 외부 패키지를 구분해서 범위를 나눌 수 있어요.