모듈

모듈 (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)"가 될 수도 있어요. 같은 경로를 가진 모듈이 여럿 있을 때 모호함을 피하는 데 쓰여요.

의사 디렉토리는 두 가지가 있어요:

  1. std: std 의사 디렉토리는 Nim 표준 라이브러리의 추상적인 위치예요. 예를 들어 import std / strutils 문법은 표준 라이브러리의 strutils 모듈을 모호함 없이 가리킬 때 써요.
  2. 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이라는 패키지 식별자가 부여돼요.

패키지의 구분 덕분에 진단용 컴파일러 메시지를 현재 프로젝트의 패키지와 외부 패키지를 구분해서 범위를 나눌 수 있어요.

출처: Nim Manual — Modules