第10章 モジュール
プログラムが育つと、1つのファイルに収まらなくなります。この章では Lune のプログラムを複数ファイルに分ける方法 — モジュールを学びます。
v0.1 のモジュール機構は意図的に小さいものです。パッケージ管理も、公開/非公開の制御も、名前空間アクセスもありません。あるのは「ファイルを分けて、読み込む」だけ。それでも、この最小の道具立てには覚えるべき規則が4つあり、間違えたときにはちゃんと専用の診断(MOD 族、第11章)が出ます。
10.1 module 宣言 — ファイルの名札
第1章から書き写してきた1行目を、ようやく主役にします。
module geometry
Lune のソースファイルは、自分がどのモジュールなのかを宣言します。そして宣言はファイルの位置と一致していなければなりません — geometry.lune なら module geometry、util/text.lune なら module util.text です。ドットがディレクトリの区切りに対応します。
module util.text
def shout(s: String): String =
s + "!"
つまりモジュール名は、ファイルへの道順そのものです。名札が中身と食い違っていると、読み込む側で叱られます(10.5節)。
なお、--eval や --check に直接渡す入口のファイルだけは、宣言とパスの一致を求められません(本書がこれまで module hello を色々な場所に置けたのはこのためです)。
10.2 import — 名前を持ってくる
読む側は import です。main.lune:
module main
import geometry
import util.text
let area = circleArea(1.0)
let banner = shout("area")
$ lune --eval area main.lune
3.14159
$ lune --eval banner main.lune
"area!"
import geometry と書くと、geometry.lune のトップレベル宣言(def・let・type・record)がそのまま自分の環境に入ります。geometry.circleArea(...) ではなく circleArea(...) と、非修飾で呼べていることに注目してください。
修飾して呼ぶことは、v0.1 ではできません。
lune> let a = geometry.circleArea(1.0)
error[TYP0001]: 未定義の名前: geometry
= help: 詳しくは `lune explain TYP0001 --lang ja` を実行してください
geometry という名前の値は存在しない — import が運んでくるのは中身の名前だけ、というわけです。裏を返せば、別々のモジュールが同じ名前を定義していると衝突します。v0.1 では名前を短くしすぎない(total より cartTotal)のが自衛策です。
module 宣言と import の行は、間に空行を挟まず続けて書くのが正準形です(lune fmt がこの形に整えます。第12章)。
10.3 分け方の作法
どう分けるか。v0.1 の道具立てで実際に効くのは、次の2つの発想です。
データとその操作をまとめる。レコードや ADT の定義と、それを扱う関数を同じモジュールに置きます。演習 10-2 で作る shop/items.lune がこの形です。
module shop.items
record Item:
name: String
price: Int
def priceOf(item: Item): Int =
item.price
def total(items: List[Item]): Int =
fold(map(items, priceOf), 0, fn a x -> a + x)
汎用の道具を切り出す。文字列や数値のユーティリティのように、どこからでも呼ばれるものを util.text のような場所へ。
逆に、v0.1 ではやらないほうがよい分け方もあります。「実装を隠すために分ける」— 公開/非公開の制御がないので、分けても全部見えます。「循環しそうな相互参照を分ける」— 次節のとおり、循環は禁止です。
10.4 循環 import は検出される
cycle_a が cycle_b を import し、cycle_b が cycle_a を import したらどうなるか。
$ lune --check cycle_a.lune
error[MOD0002]: モジュールの循環 import を検出しました: cycle_a.lune -> cycle_b.lune -> cycle_a.lune
--> cycle_b.lune:2:1
|
2 | import cycle_a
| ^^^^^^ この import が循環を閉じている
= help: 詳しくは `lune explain MOD0002 --lang ja` を実行してください
診断が循環の経路をそのまま(cycle_a → cycle_b → cycle_a)見せ、しかも「輪を閉じた import」を名指ししてくれます。どこを切ればよいか一目で分かる形です。
なぜ禁止なのか。Lune は依存モジュールを先に型検査して評価します(main を検査する前に geometry を検査する)。循環があると「先」が決められません。第4章で let x = x + 1 が RUN0005 になったのと同じ理屈が、ファイルの粒度で現れたものです。
直し方は3つ。共通部分を第3のモジュールに抜き出す、依存の向きを片方だけに直す、あるいは2つを1つのファイルに戻す(同じファイル内の相互参照は問題ありません)。
10.5 モジュールが見つからないとき — 探索の順番
import foo.bar は foo/bar.lune を探します。探す場所は3つ、この順です。
- 入口ファイルのあるディレクトリ
- カレントディレクトリ
--module-pathで足したディレクトリ(複数指定可)
3番目が便利なのは、共有ライブラリを別ディレクトリに置きたいときです。lib/shared.lune(module shared)を usesshared.lune から使う例:
$ lune --check usesshared.lune
error[MOD0001]: モジュールが見つかりません: shared
$ lune --module-path lib --check usesshared.lune
type check OK
$ lune --module-path lib --eval answer usesshared.lune
42
見つからないときの診断は、探した場所を教えてくれます。
error[MOD0001]: モジュールが見つかりません: nothere
--> missing.lune:2:1
|
2 | import nothere
| ^^^^^^ 対応する .lune ファイルが見つからない
= hint: 検索した場所: .
= help: 詳しくは `lune explain MOD0001 --lang ja` を実行してください
(検索した場所 は実際には絶対パスで表示されます。紙面では省略しています。)
ファイルは見つかったのに名札が違う場合は、別の診断です。mismatch.lune の中身が module totally.different だったとき:
error[MOD0003]: module 宣言の不一致: mismatch のはずが totally.different でした
--> mismatch.lune:1:1
|
1 | module totally.different
| ^^^^^^ モジュール名が import パスと一致しない
= hint: 宣言を `module mismatch` に直すか、`totally.different` として import してください
= help: 詳しくは `lune explain MOD0003 --lang ja` を実行してください
hint が両方向の直し方(名札を直す / import を直す)を示していることに注目してください。どちらが正しいかは設計判断なので、機械は決めずに両方を並べる — 第11章で学ぶ「hint は原因と対処」の good example です。
10.6 外部 import — java.* と std.*
最後に、Lune のファイルを指さない import があります。
module basics
import java.time.LocalDate
def today(): String =
LocalDate.now().toString()
java.*・javax.*・kotlin.*・std.* で始まる import は外部 importとして扱われ、ファイル解決されません。型検査は末尾の名前(LocalDate)を Any として登録するだけなので、この定義は型検査を通ります — が、実際に呼び出せば実行時に失敗します。JVM 連携は将来仕様(付録E)で、v0.1 では「構文だけ受け付ける」段階です。
Any として通ってしまう以上、外部 import は型検査の保護が効かない領域です。v0.1 で書くプログラムでは使わないのが賢明です。
壊してみよう — この章の3つの
MOD診断のうち、MOD0001(見つからない)とMOD0003(名札不一致)を自分で出してみてください。片方から他方へ移る実験もできます:import nothereのままnothere.luneを作り、中身をmodule wrong.nameと書けば、MOD0001がMOD0003に変わります。error[MOD0003]: module 宣言の不一致: nothere のはずが wrong.name でした --> nothere.lune:1:1 | 1 | module wrong.name | ^^^^^^ モジュール名が import パスと一致しない = hint: 宣言を `module nothere` に直すか、`wrong.name` として import してください = help: 詳しくは `lune explain MOD0003 --lang ja` を実行してください「ファイルを見つける」と「名札を照合する」が別の段階だと体感できます。なお
module something.elseと書くとelseが予約語なので構文エラー(PRS0002)— モジュール名にキーワードは使えません。
まとめ
| 概念 | 一言で |
|---|---|
module a.b | 名札。a/b.lune と一致必須(入口ファイルは例外) |
import a.b | 相手のトップレベル宣言が自分の環境に入る(非修飾で使う) |
| 名前空間アクセス | a.b.f() は未対応。名前の衝突には自分で気をつける |
| 正準形 | module と import は空行を挟まず続ける |
| 循環 | MOD0002。依存は先に評価されるので輪は作れない |
| 探索順 | 入口ファイルの場所 → カレント → --module-path |
| 診断 | MOD0001 見つからない(探した場所つき)/ MOD0003 名札不一致(両方向の hint) |
| 外部 import | java.* std.* などは Any 登録のみ。v0.1 では実質使えない |
演習問題
演習 10-1(★) import nothere と書いたファイルを作って MOD0001 を出し、次に nothere.lune を作って中身を module wrong.name にして MOD0003 に変えてください。
解答
本文 10.5 節と「壊してみよう」の実例そのままです。MOD0001 の hint は「探した場所」、MOD0003 の hint は「名札を直す / import を直す」の2択。ファイル解決 → 名札照合の順に検査が進んでいることが、診断の変化から読み取れます。
演習 10-2(★★) 第6章の Item レコードと集計処理を shop/items.lune(module shop.items)に切り出し、別ファイルの main から使って合計を求めてください。
解答
module shop.items
# 演習 10-2: データとその操作をモジュールに切り出す。
record Item:
name: String
price: Int
def priceOf(item: Item): Int =
item.price
def total(items: List[Item]): Int =
fold(map(items, priceOf), 0, fn a x -> a + x)
module shop_main
import shop.items
# import した名前は非修飾で使える。Item も priceOf も total も。
let cart = [Item(name = "pen", price = 120), Item(name = "note", price = 200)]
let sum = total(cart)
$ lune --eval sum shop_main.lune
320
record Item も import で運ばれてくるので、使う側は Item(name = ..., price = ...) と普通に構築できます。型・関数・値の区別なく、トップレベル宣言はすべて渡ってきます。
演習 10-3(★★) lib/ ディレクトリに module shared のファイルを置き、--module-path を使って読み込んでください。--module-path を付けない場合との違いも確認すること。
解答
本文 10.5 節のとおりです。付けなければ MOD0001、付ければ通ります。lib/shared.lune は module shared(module lib.shared ではない)である点に注意 — --module-path lib は「lib を探索の起点にする」という指定なので、lib はモジュール名の一部になりません。
演習 10-4(★★★) 2つのモジュールで循環を作って MOD0002 を出し、そのうえで共通部分を第3のモジュールに抜き出して解消してください。
解答
cycle_a ⇄ cycle_b で循環を作れます(本文 10.4 節)。解消は、両者が必要とする定義を common.lune に移し、cycle_a と cycle_b の両方が common だけを import する形にします。依存グラフが「輪」から「木」に変わるので、評価順が決まるようになります。診断が経路(a -> b -> a)を見せてくれるので、どの辺を切るかの判断はそこから始めるとよいでしょう。
演習 10-5(★) import java.time.LocalDate を書いたファイルは型検査を通ります。なぜでしょうか。また、それが安全でないのはなぜでしょうか。
解答
外部 import は末尾名 LocalDate を Any として登録するだけで、実体を解決しないからです(10.6節)。Any は何にでも使えてしまうので、LocalDate.nonexistentMethod() のような誤りも型検査を通り抜け、実行時まで問題が見つかりません。型検査の保護が効かない — v0.1 で外部 import を避ける理由です。
より正確には — 探索 root の決定、外部 import の判定、読み込み順と循環検出の規範は documents/MODULE_LOADING_SPEC.md。MOD 族の診断は documents/ERROR_INDEX_JA.md にも収録されています。この章のコード例は books/examples/ch10/ にあり、すべて実際の CLI で検証されています。