# 模块与导入
Deft 的模块系统基于文件路径。模块是按路径加载的 `.dft` 文件;顶层声明构成模块的公开接口(`def` 引入模块作用域的绑定)。`implements` 声明会在导入时添加模块级一致性检查。
## `import` — 加载模块
`import "path" as alias` 加载模块并将其公开接口绑定到命名空间别名:
import "deft/stdext/async" as async set results [async/await-all $futures]
导入路径有多种形式:
- 相对路径 — `"./helpers"` / `"../lib/util"` 相对于导入文件所在目录解析。
- 限定包名 — `"owner/name/sub/path"`(例如
`deft/ui/components/button`)通过从 `:pkgs` 根目录自动注册的包挂载
解析。可在名称后追加 `@version` 进行固定(`deft/ui@0.1.0/...`);
`@latest` 是未固定解析的显式别名。
- 挂载名 — 任何其他与预置文件 `:mounts` 条目或
`[pkg/mount ...]` 注册匹配的首段。最长挂载前缀优先,因此限定包名
不会被单段挂载遮蔽。
- 相对于当前工作目录 — 形如 `"foo/bar"` 的普通路径若其文件存在于
工作目录下,会先在本地解析,然后才查询挂载。
路径中省略 `.dft` 扩展名。限定包导入失败时会报告可用版本
(`no matching version; available: ...`);仅给出包前缀时会提示限定写法
(`did you mean "deft/ui/..."?`)。
### 不导入直接解析
`[import/resolve "path"]` 在不加载的情况下回答“`import` 会加载哪个
文件?”—— 使用相同的解析器和优先级(相对路径、相对于当前工作目录、
带版本固定的挂载、stdext 回退):
[import/resolve "deft/ui/components/button"] # => ".deft/packages/deft/ui/0.1.0/src/components/button.dft" [import/resolve "deft/ui@0.1.0/components/button"] # explicit pin [import/resolve "./helpers"] # relative to caller
返回字面文件路径(包含 `loadFile` 会使用的 `.dft` 后缀 ——
`.../markdown.syntax` 解析为 `.../markdown.syntax.dft`),若导入无法解析
则返回 nil。适用于工具:跳转到源码、文件监视、依赖图、诊断。
包的资源路径也可解析 —— 即 `import` 本身不会加载的目录和包根文件。
限定引用会先在该包代码目录下探测子路径是否为目录,然后在包根目录下
探测;单独的 `"owner/name"` 返回包根本身:
[import/resolve "deft/help/manual"] # → "packages/help/manual" (directory) [import/resolve "deft/help/help.md"] # → "packages/help/help.md" (root file) [import/resolve "deft/help"] # → "packages/help" (package root)
因此,需要包数据目录(文档树、主题、资源)的应用可以从代码导入所用的
同一 `owner/name` 标识推导出该目录 —— 无需针对每种安装布局手写路径
探测。
### `from ... import`
`from "path" import @{ name1 name2 }` 将单个名称拉入当前作用域,无需
别名:
from "deft/stdext/async" import @{ await-all map-concurrently } set xs [await-all $futures]
名称列表是 Deft 列表字面量 —— 多个名称用 `@{...}`,单个符号也可以。
## 命名空间
每个运行时都有 `current-ns` —— 顶层 `def` 所进入的命名空间。默认
命名空间是 `"user"`。
[current-ns] # => "user" [in-ns "my-app"] # switch namespace
`in-ns` 在脚本中很少使用;它主要用于 REPL 会话以及动态管理命名空间的
运行时/工具代码。
### 模块命名空间
当你 `import "foo/bar" as bar` 时,加载模块的命名空间可通过
`bar/name` 访问:
# In stdext/async.dft: def await-all {|futures| ... } # In your script: import "deft/stdext/async" as async [async/await-all $futures] # ← name resolved through alias
## `implements` — 模块符合性
模块可以声明它符合某个接口。在导入时,Deft 会验证接口要求的所有函数
都已导出:
# my-app/storage.dft implements StorageInterface def get {|key| ... } def put {|key val| ... } def delete {|key| ... }
如果缺少必需函数,导入会失败并报错,错误中会指出缺失的函数,且脚本
不会执行失败导入之后的代码:
没有 `implements` 声明的模块没有一致性要求。符合接口的模块可以导出
接口要求之外的额外函数。
> `implements` 是模块级一致性检查(该文件是否导出了所需名称?)。
> 它不同于 `defprotocol`/`defimpl`,后者是管理值上方法分派的类型级
> 协议。参见类型与协议。
## 定义接口
接口是一个声明所需导出列表的模块。最简单的形式是记录预期名称的模块;
Deft 的检查器会遍历导入并查找这些名称。
# stdext/storage_iface.dft # # Modules implementing StorageInterface must export: get, put, delete. def required-names ^{:get :put :delete}
真正的接口模块通常还会定义实现者需要的类型或文档。规范示例参见
`test/test_impls.dft`。
## 包
包是包含 `package.dft` 清单的 Deft 模块目录。包是分发单位:通过
`pkg/install` 安装,挂载到运行时,并从任意模块导入。
def manifest %Package{ :owner "alice" :name "math-lib" :version "1.0.0" :description "Personal math helpers" }
包清单格式、`pkg/*` 标准库函数以及 `.dfs` 分发文件参见包。
## 搜索路径
未限定路径按以下顺序解析:
1. 相对于当前工作目录(`<cwd>/foo/bar.dft` 或其目录索引形式),仅当
文件存在时。
2. 挂载名(最长前缀优先):来自 `:pkgs` 自动扫描的限定包挂载
`owner/name[@version]`,然后是单段预置文件 `[pkg/mount]` 挂载。
3. 对 `syntax/*` 模块的 stdext 子路径回退。
`:pkgs` 根目录和包引用语法参见包。
## 循环导入
Deft 不支持循环导入。如果模块 A 导入 B,而 B 又导入 A,加载器会在
加载时检测到循环并抛出错误。请重构以打破循环,或使用 `pub/*` 在运行时
于模块间通信。
## 重新加载
`[reload "path"]` 重新加载磁盘上已更改的模块。在开发期间很有用(尤其
是在 REPL 或长时间运行的 TUI 应用中)。对旧定义的现有引用不会更新;
只有后续查找才会解析到新版本。
[reload "my-app/foo"]
## 已加载模块的信息
`info/*` 命名空间公开模块表:
[info/modules] # list of loaded module names [info/defs] # defs in the current namespace [info/vars] # vars in the current namespace [info/resolve "name"] # resolve a name to its source
完整的 `info/*` API 参见元编程。
## 快速参考
| 形式 | 用途 |
|---|---|
| `import "path" as alias` | 加载模块,绑定到别名 |
| `from "path" import @{ names }` | 引入指定名称 |
| `[in-ns "name"]` | 切换当前命名空间 |
| `[current-ns]` | 获取当前命名空间名称 |
| `implements InterfaceName` | 声明模块一致性(加载时检查) |
| `[info/modules]` | 列出已加载模块 |
| `[reload "path"]` | 重新加载模块 |