Scheme 模块:define-module 与 import
在 Guile Scheme 中组织代码,核心手段是使用模块系统。模块让你把相关函数、变量打包成独立单元,并通过 define-module 声明模块内容,用 import 引入其他模块的功能。掌握这两者,就能写出结构清晰、可复用的 Scheme 程序。
创建自己的模块
定义一个模块,需要使用 define-module 表达式。它必须放在文件顶部(或 REPL 的最开始),格式如下:
(define-module (mylib math utils)
#:export (add multiply))
(mylib math utils)是模块名,采用层级命名法,类似文件路径。#:export后列出你希望对外公开的符号(函数、变量等)。未导出的内容仅在模块内部可见。
步骤:创建并导出函数
- 新建一个文件,命名为
utils.scm。 - 写入以下内容:
(define-module (mylib math utils)
#:export (add multiply))
(define (add a b)
(+ a b))
(define (multiply a b)
(* a b))
- 保存文件。此时,
add和multiply成为该模块的公共接口。
注意:模块名
(mylib math utils)并不要求对应真实目录结构,但 Guile 默认会按此路径查找文件(如mylib/math/utils.scm)。为简化,可将文件放在当前目录,并在启动 Guile 时设置%load-path。
使用其他模块
要使用已定义的模块,需用 import 引入。import 的语法直接指定模块名。
步骤:导入并调用模块函数
- 启动 Guile REPL 或新建主程序文件
main.scm。 - 添加当前目录到模块搜索路径(如果
utils.scm在当前目录):
(add-to-load-path ".")
- 导入模块:
(import (mylib math utils))
- 调用导出的函数:
(add 3 5) ; 返回 8
(multiply 4 6) ; 返回 24
模块命名与文件布局
Guile 根据模块名自动推断文件位置。例如:
- 模块
(mylib math utils)→ 默认查找mylib/math/utils.scm - 模块
(json)→ 查找json.scm
若不想遵循默认路径,可通过 %load-path 手动指定:
(use-modules (ice-9 popen)) ; 先引入必要模块
(add-to-load-path "/path/to/your/modules")
(import (mylib math utils))
控制导出内容
模块内部定义的所有绑定默认私有。只有显式 #:export 的才能被外部访问。
示例:混合公有与私有函数
(define-module (mylib string helpers)
#:export (safe-capitalize))
(define (internal-helper s)
(string-downcase s))
(define (safe-capitalize str)
(if (string? str)
(string-titlecase (internal-helper str))
""))
在此模块中:
safe-capitalize可被外部调用。internal-helper无法从模块外访问,即使你知道它的名字。
导入策略:选择性与重命名
有时你只想导入部分符号,或避免名称冲突。Guile 提供精细控制。
仅导入特定符号
(import (only (mylib math utils) add))
; 此时 multiply 不可用
重命名导入符号
(import (rename (mylib math utils) (add my-add)))
; 现在用 my-add 调用原 add 函数
(my-add 1 2) ; 返回 3
组合使用
(import (only (rename (srfi srfi-1) (fold fold-left)) fold-left))
常见错误与排查
| 错误现象 | 可能原因 | 解决方法 |
|---|---|---|
no code for module (xxx) |
模块文件未找到 | 确认文件名与模块名匹配,检查 %load-path 是否包含文件所在目录 |
unbound variable: xxx |
符号未导出或拼写错误 | 核对 #:export 列表,确保导入时名称一致 |
import: invalid module spec |
import 语法错误 |
确保模块名用括号包裹,如 (import (a b c)),而非 (import a b c) |
实战:构建多文件项目
假设你要开发一个简单计算器,拆分为两个模块。
- 创建
ops.scm:
(define-module (calc ops)
#:export (+ - * /))
(define (+ a b) (primitive+ a b))
(define (- a b) (primitive- a b))
(define (* a b) (primitive* a b))
(define (/ a b) (if (zero? b) (error "Divide by zero") (primitive/ a b)))
- 创建
ui.scm:
(define-module (calc ui)
#:export (run-calculator))
(import (calc ops)
(ice-9 format))
(define (run-calculator)
(format #t "Result: ~a\n" (+ 10 (* 3 4))))
- 主程序
app.scm:
(add-to-load-path ".")
(import (calc ui))
(run-calculator) ; 输出: Result: 22
运行命令:
guile app.scm
模块初始化与副作用
模块顶层表达式只在首次导入时执行一次。适合做一次性初始化。
(define-module (mylib config)
#:export (config-data))
(format #t "Loading config...\n") ; 仅打印一次
(define config-data
'((host . "localhost") (port . 8080)))
多次 import 不会重复执行 format 语句。
使用标准库模块
Guile 自带大量模块,无需额外安装。常用示例:
(ice-9 format):提供format字符串格式化。(srfi srfi-1):列表处理函数(如fold,filter)。(rnrs io ports):端口 I/O 操作。
导入标准库:
(import (srfi srfi-1))
(filter even? '(1 2 3 4 5)) ; 返回 (2 4)
暂无评论,快来抢沙发吧!