21 모듈 — 감춘 것이 기본이다
먼저 알아야 할 것
돌아보기
16장의 마지막 사례에서, 들여온 라이브러리가 네트워크에 닿는지 알려면 무엇을 보면 된다고 했는가?
답. 그 라이브러리가 내보낸 op 의 머리를 훑어 cap net(그리고 cap c·cap machine)을 받는 op 이 있는지 보면 된다고 했다. 이 장은 그 “내보낸” 이 무엇이고, 다른 파일의 이름을 어떻게 들여오는지를 다룬다.
이 장의 필요성과 맥락
이 장이 끝나면
export 를 붙인 것만 밖에서 보인다는 것을 알게 된다. use … from "자리" 로 다른 파일을 들여오고, 여러 파일을 한 번역 단위로 넘기며, 표준 라이브러리 이름을 들여오는 법을 익힌다. 모듈 이름으로 한정해도 감춘 이름에는 닿지 못한다는 것, 검색 경로가 없다는 설계, 최상위 선언의 차례가 무관하다는 것도 보게 된다.이 장에서 답할 질문
from "…"없이use geom .이라고만 적으면 어디서 찾는가?
21.1 파일 하나, 모듈 하나#
소스 파일 하나가 모듈 하나다. 모듈은 첫 줄에 자기 이름을 적는다. 모듈 이름은 파일 이름과 같을 필요가 없다.
examples/ch21/geom.low
module geom .
export struct point do
x i64 .
y i64 .
end
export fn manhattan input a point . input b point . output i64 .
requires ge (field a x) -1000000 .
requires le (field a x) 1000000 .
requires ge (field b x) -1000000 .
requires le (field b x) 1000000 .
requires ge (field a y) -1000000 .
requires le (field a y) 1000000 .
requires ge (field b y) -1000000 .
requires le (field b y) 1000000 .
do
return add (abs (sub (field b x) (field a x))) (abs (sub (field b y) (field a y))) .
end
fn helper input v i64 . output i64 .
do
return v .
end
실행 결과
$ lowentc --check geom.low
== check: ok ==
geom 모듈은 point 구조체와 manhattan op 을 export 로 내보낸다. helper 에는 export 가 없다. 그래서 helper 는 이 모듈 안에서만 쓸 수 있다.
모듈 geom (geom.low) 모듈 app
┌────────────────────────────────────┐
│ export struct point ─────────┼──▶ 문 ──▶ geom.point ✔ 보인다
│ export fn manhattan ─────────┼──▶ 문 ──▶ geom.manhattan ✔ 보인다
│ fn helper (export 없음) │ geom.helper ✘ E-VISIBILITY
└────────────────────────────────────┘
벽 안의 것은 export 로 낸 문으로만 나간다감춘 것이 기본이다. 밖에서 보이는 것은 곧 약속이고, 실수로 약속하는 일보다 실수로 감추는 일이 고치기 쉽다. 네이티브로 낼 때도 export 한 op 만 C 에서 부를 수 있는 심볼이 되고 나머지는 모두 static 이다.
21.2 들여오기#
use <모듈> from "<자리>" . 은 그 자리의 파일에서 모듈을 들여온다. 자리는 선언한 파일을 기준으로 한 경로다.
examples/ch21/app.low
module app .
rem run: distance 3 4
use geom from "geom.low" .
fn distance input dx i64 . input dy i64 . output i64 .
requires ge dx -1000 .
requires le dx 1000 .
requires ge dy -1000 .
requires le dy 1000 .
do
let a geom.point be make geom.point do x 0 . y 0 . end .
let b geom.point be make geom.point do x dx . y dy . end .
return geom.manhattan a b .
end
실행 결과
$ lowentc --run distance app.low 3 4
distance(3, 4) = 7
들여온 이름은 모듈 이름으로 한정해 부른다 — geom.point, geom.manhattan. 붙은 점은 값에 대한 연산이 아니라 선언된 이름을 가리키는 경로다(3장).
한정해도 감춘 이름에는 닿지 못한다.
examples/ch21/hidden.low
module hidden .
rem expect: E-VISIBILITY
use geom from "geom.low" .
fn peek input v i64 . output i64 .
do
return geom.helper v .
end
실행 결과
$ lowentc --check hidden.low
hidden.low:8:0 E-VISIBILITY: this name belongs to ANOTHER module and is not `export`ed — qualifying it does not open the door. A module that cannot keep anything private is not a module, it is a prefix. Mark it `export`, or stop reaching in (RFC-0011 §6.2)
진단의 말대로 “아무것도 감출 수 없는 모듈은 모듈이 아니라 접두사다”.
문. from "…" 없이 use geom . 이라고만 적으면 어디서 찾는가?
답. 같은 번역 단위 안에서 찾는다. lowentc --check app.low geom.low 처럼 파일을 여럿 넘기면 그 파일들이 하나의 단위가 된다. 그 단위에 없으면 컴파일러는 W-USE-EXTERNAL 로 “이 모듈이 있는지 확인할 수 없다” 고 말하고, 그 이름을 실제로 쓰면 거절한다. 표준 라이브러리 이름(use allocs .)은 설치된 표준 모듈 자리에서 해소된다.
21.3 무엇을 내보낼 수 있고, 어떻게 줄여 부르나#
export 는 이름을 가진 선언 대부분에 붙는다. 붙지 않는 것도 있다.
| 선언 | export | 까닭 |
|---|---|---|
fn · proc | 된다 | 다른 모듈이 부를 op |
struct · enum · type · newtype | 된다 | 내보낸 op 의 입력·출력 타입도 함께 내보내야 쓸 수 있다 |
trait · actor | 된다 | 다른 모듈의 타입이 갖출 약속 · 다른 모듈이 띄울 액터 |
최상위 let(상수) | 안 된다 — E-TOPLEVEL | 상수는 모듈 안에 둔다. 밖에 알려야 하면 그 값을 돌려주는 fn 을 내보낸다 |
test | 안 된다 — E-TOPLEVEL | 시험은 그 모듈을 짓는 사람의 것이다 |
표 21.1 — export 를 붙일 수 있는 선언
모듈 이름이 길거나 두 모듈의 이름이 같으면 as 로 줄여 부른다.
examples/ch21/alias.low
module alias .
rem run: distance 3 4
rem 들여온 모듈을 짧은 이름 g 로 부른다
use geom from "geom.low" as g .
fn distance input dx i64 . input dy i64 . output i64 .
requires ge dx -1000 .
requires le dx 1000 .
requires ge dy -1000 .
requires le dy 1000 .
do
let a g.point be make g.point do x 0 . y 0 . end .
let b g.point be make g.point do x dx . y dy . end .
return g.manhattan a b .
end
실행 결과
$ lowentc --run distance alias.low 3 4
distance(3, 4) = 7
use geom from "geom.low" as g . 뒤로는 g.point·g.manhattan 으로 부른다. 별칭은 이 파일 안에서만 쓰는 이름이고, 모듈의 실제 이름(geom)은 바뀌지 않는다. 다만 이 파일 안에서는 원래 이름이 서지 않는다 — geom.point 라고 적으면 E-USE-ALIASED 다. 별칭은 덧이름이 아니라 바꿔 부르기이고, 같은 모듈을 두 철자로 부르면 읽는 사람이 둘이 같은 것인지 확인해야 한다. 같은 이름을 두 번 들여오면 E-NAME-COLLISION 이 나는데, 그 진단이 권하는 해법이 이 별칭이다.
반례. 별칭을 적어 놓고 원래 이름으로 부른다
examples/ch21/mistake_aliasold.low
module mistake_aliasold .
rem expect: E-USE-ALIASED
use geom from "geom.low" as g .
fn origin_x output i64 .
do
rem ✘ 별칭을 적어 놓고 원래 이름으로 부른다 --- 같은 모듈을 두 철자로 부르는 코드가 된다
let p geom.point be make geom.point do x 7 . y 0 . end .
return field p x .
end
실행 결과
$ lowentc --check mistake_aliasold.low
mistake_aliasold.low:9:9 E-USE-ALIASED: this unit renamed that module with `as`, so its name here is `g` — the original name no longer stands. One thing, one spelling: if both names worked, a reader would have to check that they mean the same module. Write the member under `g.`
mistake_aliasold.low:9:28 E-USE-ALIASED: this unit renamed that module with `as`, so its name here is `g` — the original name no longer stands. One thing, one spelling: if both names worked, a reader would have to check that they mean the same module. Write the member under `g.`
as g 를 적은 순간 이 파일에서 그 모듈의 이름은 g 하나다. geom.point 는 E-USE-ALIASED 로 거절된다. 두 철자가 다 서면 한쪽만 고친 코드가 생기고, 읽는 사람은 둘이 같은 모듈인지 매번 확인해야 한다. 원래 이름을 적는 자리는 use 줄 하나뿐이다.
21.4 검색 경로가 없다#
많은 언어는 이름만 적으면 도구가 어딘가의 검색 경로에서 모듈을 찾아온다. 그러면 이 프로그램이 무엇에 기대는지가 소스 밖의 지식이 되고, 같은 소스가 기계에 따라 다른 파일을 가져올 수 있다. Lowent 의 들여오기는 셋 가운데 하나다.
- 자리를 적는다:
use geom from "geom.low" . - 같은 번역 단위로 함께 넘긴다:
lowentc --check app.low geom.low - 표준 라이브러리의 예약된 이름을 쓴다:
use allocs .
패키지 매니페스트(pkg.low)로 외부 의존을 적을 때도 원칙은 같다. 의존은 내용 해시와 함께 고정되고, 바이트가 바뀌면 빌드가 거절된다(31장).
흔한 오해. 모듈 이름은 파일 이름에서 온다
use 가 찾는 것은 파일 안의 module 선언이다. lib/alloc.low 의 모듈 이름은 allocs 이고, lib/str.low 는 strings, lib/vec.low 는 vecs 다. 이름이 다른 모듈의 목록은 표준 라이브러리 부(32장)의 첫 표에 있다.21.5 이름이 부딪히면#
한 모듈 안에서 같은 이름을 두 번 선언할 수 없다.
examples/ch21/dup.low
module dup .
rem expect: E-NAME-DUP
fn answer output u32 . do return 1 . end
fn answer output u32 . do return 2 . end
실행 결과
$ lowentc --check dup.low
dup.low:5:4 E-NAME-DUP: `answer` (op) is declared twice in module `dup` — a qualifier cannot tell the two apart (both are `<module>.<name>`), so the second does not hide the first: it silently makes one of them unreachable
두 번째가 첫 번째를 가리는 것이 아니라, 둘 가운데 하나에 조용히 닿을 수 없게 된다. 그래서 거절한다. 들여온 이름끼리 부딪히면 부르는 자리에서 모듈 이름으로 밝혀 적는다. 들여오기는 이미 있는 이름을 덮어쓰지 않는다(E-NAME-COLLISION).
21.6 최상위 선언의 차례는 무관하다#
모듈 안의 최상위 선언은 차례와 상관이 없다. 뒤에 선언한 op 을 앞에서 부를 수 있고, 서로 부르는 op 도 된다.
examples/ch21/order.low
module order .
rem run: is_even 10
rem run: is_even 7
fn is_even input n u32 . output bool .
requires le n 1000 .
do
if eq n 0 . do return true . end
return is_odd (sub n 1) .
end
fn is_odd input n u32 . output bool .
requires le n 1000 .
do
if eq n 0 . do return false . end
return is_even (sub n 1) .
end
실행 결과
$ lowentc --run is_even order.low 10
is_even(10) = 1
$ lowentc --run is_even order.low 7
is_even(7) = 0
is_even 은 뒤에 선언된 is_odd 를 부르고, is_odd 는 다시 is_even 을 부른다. 반면 op 본문 안의 지역 이름은 쓰기 전에 선언해야 한다. 파일 전체는 훑어보며 읽지만 본문은 위에서 아래로 읽기 때문이다.
21.7 흔한 실수#
반례. 들여온 이름을 모듈 이름 없이 부른다
examples/ch21/mistake_bare.low
module mistake_bare .
rem expect: E-VISIBILITY
use geom from "geom.low" .
fn origin_x output i64 .
do
rem ✘ 들여온 이름을 모듈 이름 없이 불렀다
let a point be make point do x 0 . y 0 . end .
return field a x .
end
실행 결과
$ lowentc --check mistake_bare.low
mistake_bare.low:9:0 E-VISIBILITY: this name is another module's export, reached BARE — a cross-module name must be QUALIFIED: write `<module>.<name>` (RFC-0011 qualified-by-default; there is no glob import)
mistake_bare.low:9:0 E-VISIBILITY: this name is another module's export, reached BARE — a cross-module name must be QUALIFIED: write `<module>.<name>` (RFC-0011 qualified-by-default; there is no glob import)
어떤 언어는 들여온 이름을 그냥 쓰게 하거나 import * 로 한꺼번에 풀어놓는다. 그러면 point 가 이 파일의 것인지 어느 모듈의 것인지 읽어서 알 수 없고, 들여오는 모듈이 늘면 이름이 조용히 부딪힌다. Lowent 에는 풀어놓기가 없다. 다른 모듈의 이름은 언제나 geom.point 처럼 한정해 적는다. 진단이 같은 줄에 두 번 나오는 것은 let 의 타입 자리와 make 자리를 따로 세기 때문이다.
반례. from 의 자리에 확장자를 빠뜨린다
examples/ch21/mistake_noext.low
module mistake_noext .
rem expect: E-DEP-MISSING
rem ✘ 자리는 파일 이름 그대로 --- `.low` 까지 적는다
use geom from "geom" .
fn same input v i64 . output i64 .
do
return v .
end
실행 결과
$ lowentc --check mistake_noext.low
E-DEP-MISSING: `use geom from "geom"` — cannot read that source. A dependency the tool cannot see is a dependency nobody checked
자리는 짐작되지 않는다. "geom" 이라고 적으면 도구는 정확히 geom 이라는 파일을 찾고, 없으면 E-DEP-MISSING 이다. 검색 경로가 없는 것과 같은 까닭이다 — 도구가 .low 를 붙여 보거나 다른 디렉터리를 뒤지기 시작하면 무엇에 기대는지가 다시 소스 밖으로 나간다. use geom from "geom.low" . 처럼 파일 이름을 그대로 적는다.
반례. 파일 이름으로 표준 모듈을 들여온다
examples/ch21/mistake_filename.low
module mistake_filename .
rem expect: E-IR-UNDEF
rem ✘ 파일은 `lib/str.low` 이지만 그 안의 모듈 이름은 `strings` 다
use str .
fn same input a slice u8 . input b slice u8 . output bool .
do
return str.eq_str a b .
end
실행 결과
$ lowentc --check mistake_filename.low
mistake_filename.low:5:0 W-USE-EXTERNAL: this module is not in the compilation unit — there is no module search path, so nothing here can confirm it exists. Pass the file that declares it and it WILL be checked (several .low files link into one unit)
mistake_filename.low:9:0 E-IR-UNDEF: undefined name `str.eq_str` — `str` is not a module used by this unit (`use str .`) nor an enum
lib/str.low 파일의 모듈 이름은 strings 다. use str . 은 그런 모듈을 찾지 못해 W-USE-EXTERNAL 로 “확인할 수 없다” 고 알리고, 이름을 실제로 쓰는 자리에서 E-IR-UNDEF 로 거절된다. 경고를 먼저 보고 모듈 이름을 확인한다.
examples/ch21/filename_fixed.low
module filename_fixed .
rem run: same [104,105] [104,105]
rem run: same [104,105] [104,111]
use strings .
fn same input a slice u8 . input b slice u8 . output bool .
do
return strings.eq_str a b .
end
실행 결과
$ lowentc --run same filename_fixed.low [104,105] [104,105]
same([104,105], [104,105]) = 1
arg0 (written) = [104,105]
arg1 (written) = [104,105]
$ lowentc --run same filename_fixed.low [104,105] [104,111]
same([104,105], [104,111]) = 0
arg0 (written) = [104,105]
arg1 (written) = [104,111]
반례. 내보낸 op 의 서명에 감춘 타입을 쓴다
examples/ch21/secretbox.low
module secretbox .
rem expect: W-EXPORT-HIDDEN
rem `export` 가 없는 타입 --- 이 모듈 밖에서는 이름을 적을 수 없다
struct secret do
v u64 .
end
rem ✘ 내보낸 op 이 감춘 타입을 돌려준다 --- 들여온 쪽은 그 이름을 적을 수 없다
export fn make_secret output secret .
do
return make secret do v 1 . end .
end
실행 결과
$ lowentc --check secretbox.low
secretbox.low:10:0 W-EXPORT-HIDDEN: this op is exported and its signature names a type this module keeps to itself. An importing module cannot write that type, so it has nowhere to put the value and the export cannot be used from outside (§6.10.1). Export the type too, or give the op a signature made of types the other side can name
secretbox 자체는 번역을 통과하지만 내보내는 쪽에서 W-EXPORT-HIDDEN 으로 알린다 — 이 export 는 밖에서 쓸 수 없다는 뜻이다. 그 알림이 없다면 문제는 들여오는 쪽에서야 드러난다.
examples/ch21/mistake_privtype.low
module mistake_privtype .
rem expect: E-VISIBILITY
use secretbox from "secretbox.low" .
fn peek output u64 .
do
rem ✘ 받은 값을 담을 타입 이름이 감춰져 있다
let s secretbox.secret be secretbox.make_secret .
return field s v .
end
실행 결과
$ lowentc --check mistake_privtype.low
mistake_privtype.low:9:0 E-VISIBILITY: this name belongs to ANOTHER module and is not `export`ed — qualifying it does not open the door. A module that cannot keep anything private is not a module, it is a prefix. Mark it `export`, or stop reaching in (RFC-0011 §6.2)
secretbox.low:10:0 W-EXPORT-HIDDEN: this op is exported and its signature names a type this module keeps to itself. An importing module cannot write that type, so it has nowhere to put the value and the export cannot be used from outside (§6.10.1). Export the type too, or give the op a signature made of types the other side can name
make_secret 은 내보냈지만 그 결과의 타입 secret 은 감췄다. 들여온 쪽은 결과를 담을 이름을 적을 수 없어 E-VISIBILITY 로 거절된다. 쓸 수 없는 export 인 셈이다. 내보낸 op 의 입력·출력 타입도 함께 export 한다.
반례. 다른 모듈의 열거형 갈래를 case 에 모듈 이름과 함께 적는다
examples/ch21/sizes.low
module sizes .
rem 다른 모듈이 쓸 열거형 — 갈래는 값을 지니지 않는다
export enum kind do
small .
big .
end
export fn classify input n u64 . output kind . do
if lt n 100 . do
return small .
end
return big .
end
실행 결과
$ lowentc --check sizes.low
== check: ok ==
sizes 모듈은 열거형 kind 와 그것을 돌려주는 classify 를 내보낸다. 들여온 쪽에서 갈래를 모듈 이름으로 한정해 적으면 이렇게 된다.
examples/ch21/mistake_enumcase.low
module mistake_enumcase .
rem run: describe 5
rem run: describe 500
use sizes from "sizes.low" .
fn describe input n u64 . output u64 . do
let k sizes.kind be sizes.classify n .
match k do
rem ✘ 모듈 이름을 붙인 갈래 — 이 판에서는 어떤 값에나 맞는다
case sizes.small do
return 1 .
end
case sizes.big do
return 2 .
end
end
end
실행 결과
$ lowentc --run describe mistake_enumcase.low 5
describe(5) = 1
$ lowentc --run describe mistake_enumcase.low 500
describe(500) = 2
describe 500 은 big 이라 2 를, describe 5 는 1 을 돌려준다. 한정해 적어도 갈래로 읽히기 때문이다. 2026-09-16 까지는 그렇지 않았다 — 하강이 case sizes.small 을 갈래가 아니라 아무 값에나 맞는 자리로 읽어 첫 갈래가 모든 값을 가져갔고, 500 도 1 을 냈다. 검사기는 한정 이름을 갈래로 좁히는데 하강은 그러지 않아 생긴 어긋남이었고, 이제 두 층이 같은 나무를 본다. 그래도 case 에는 갈래 이름만 적는 편이 짧고, 어느 열거형의 갈래인지는 k 의 타입이 정한다.
examples/ch21/enumcase_fixed.low
module enumcase_fixed .
rem run: describe 5
rem run: describe 500
use sizes from "sizes.low" .
fn describe input n u64 . output u64 . do
let k sizes.kind be sizes.classify n .
match k do
rem 갈래 이름만 적는다 — 타입(sizes.kind)이 어느 갈래인지 정한다
case small do
return 1 .
end
case big do
return 2 .
end
end
end
실행 결과
$ lowentc --run describe enumcase_fixed.low 5
describe(5) = 1
$ lowentc --run describe enumcase_fixed.low 500
describe(500) = 2
흔한 오해. 두 모듈이 서로를 들여오면 안 된다
examples/ch21/cycle_a.low
module cycle_a .
rem run: fa 3
rem run: fa 4
rem `cycle_b` 를 들여오고, `cycle_b` 도 이 모듈을 들여온다
use cycle_b from "cycle_b.low" .
export fn fa input n u32 . output u32 .
requires le n 10 .
do
if eq n 0 . do return 0 . end
return cycle_b.fb (sub n 1) .
end
실행 결과
$ lowentc --run fa cycle_a.low 3
fa(3) = 1
$ lowentc --run fa cycle_a.low 4
fa(4) = 0
cycle_a 는 cycle_b 를 들여오고 cycle_b 도 cycle_a 를 들여오지만 번역되고 돈다. C 의 헤더처럼 “먼저 읽은 것만 안다” 가 아니라, 번역 단위 전체를 모은 뒤 이름을 해소하기 때문이다. 최상위 선언의 차례가 무관한 것과 같은 원리다. 그래도 서로 들여오는 모듈은 하나로 합치거나 공통 부분을 셋째 모듈로 빼는 편이 읽기 쉽다. 되부름의 깊이는 계약(requires le n 10 .)이 막는다.
21.8 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
module geom . | 이 파일이 모듈 geom 이다(첫 줄) | 모듈 이름은 파일 이름과 따로 — use 는 이 이름을 찾는다 |
export fn manhattan … · export struct point … | 밖에서 보이게 한다 | 감춘 것이 기본 — 보이는 것은 약속 |
use geom from "geom.low" . | 선언한 파일 기준의 자리에서 들여온다 | 검색 경로가 없다 — 무엇에 기대는지 소스에 보인다 |
use allocs . | 같은 번역 단위나 표준 라이브러리에서 들여온다 | 없으면 W-USE-EXTERNAL |
geom.point · geom.manhattan a b | 들여온 이름은 모듈 이름으로 한정한다 | 풀어놓기가 없다 |
geom.helper(감춘 이름) | 거절(E-VISIBILITY) | 한정해도 문이 열리지 않는다 |
| 같은 이름을 두 번 선언 · 두 들여오기가 같은 이름 | 거절(E-NAME-DUP · E-NAME-COLLISION) | 어느 쪽에 닿는지 조용히 정하지 않는다 |
| 최상위 선언의 차례 | 무관 — 서로 불러도 된다 | 파일은 훑어 읽고 본문은 위에서 아래로 읽는다 |
use geom from "geom.low" as g . | 들여온 모듈을 이 파일에서 g 로 부른다 | 긴 이름 · 같은 이름을 풀어 준다 — 모듈의 실제 이름은 그대로 |
export let … · export test … | 거절(E-TOPLEVEL) | 상수는 값을 돌려주는 fn 으로 내보낸다 |
다른 모듈 열거형의 case small | 갈래 이름만 적는다 | case sizes.small 은 이 판에서 아무 값에나 맞는다 |
표 21.2 — 모듈의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
export 한 것만 밖에서 보인다. use <모듈> from "<자리>" . 는 선언한 파일 기준의 자리에서 들여오고, 자리를 적지 않으면 같은 번역 단위나 표준 라이브러리에서 찾는다. 검색 경로는 없다. 한정해도 감춘 이름에는 닿지 못하고, 같은 이름의 중복 선언은 거절된다. 최상위 선언의 차례는 무관하지만 본문의 지역은 먼저 선언한다.