迁移基础 · 运行与表达 · LESSON 03

函数、模块与错误处理:让代码可组合

比较 import/export、模块边界、返回值和异常处理的工程取舍。

10 分钟functions · modules · errors

从“能调用”到“可组合”

JS/TS 的函数是一等值,import/export 让依赖关系在文件之间可见;Python 的模块也是运行时对象,导入会执行模块顶层代码;C++ 通常把声明放进头文件、实现放进源文件,再由链接器组合目标文件。迁移的关键不是把箭头函数逐字换成 def,而是保留“一个函数只承担一个可命名意图”,并让调用方知道失败如何返回。

例如“读取传感器并转换单位”可以拆成 read_rawparse_readingto_celsius。这样 Python 的数据类、C++ 的 struct 或未来的 ROS 消息都能接入同一个纯转换函数。把文件、网络或设备调用藏在纯函数内部,会让测试不得不依赖真实世界,也会让错误堆栈失去边界。

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
export function parsePort(value: string): number {
return Number.parseInt(value, 10);
}
Python
def parse_port(value: str) -> int:
  return int(value)

学习目标

  • 能把纯转换与文件、网络、时钟等副作用分开,并为依赖提供注入点。
  • 能定义函数输入、输出、不变量和失败方式,让调用方不必猜测。
  • 能验证同一核心逻辑可由 CLI、HTTP handler 或设备节点调用。

迁移时先拆纯逻辑和副作用

纯函数接收输入并返回输出,适合三种语言直接测试;读文件、发请求和写日志属于副作用,应放在边缘模块。异常不是“更高级的返回值”,它是控制流的一部分,必须决定在哪一层捕获、补充上下文或继续抛出。对单个非法传感器值可以返回校验错误,对无法连接设备则可以抛出或返回状态,让上层决定重试还是退出。

def celsius(value: float, unit: str) -> float:
    if unit == "C":
        return value
    if unit == "F":
        return (value - 32.0) * 5.0 / 9.0
    raise ValueError(f"unsupported unit: {unit}")

def load_temperature(source) -> float:
    raw = source.read()
    return celsius(float(raw.value), raw.unit)

模块边界与导入副作用

Python 模块顶层不应连接数据库或启动线程,否则测试只导入函数就会改变环境;C++ 头文件要避免把非 inline 的定义重复放入多个翻译单元,否则会出现重复符号;Node 模块要警惕循环依赖导致未完成的导出对象。把组装动作集中到入口,核心模块只暴露少量清楚的函数。

常见错误与排错思路

遇到“函数明明存在却不可调用”,先检查导出名、导入路径和循环依赖,再检查是否把默认导出当成命名导出。遇到错误被吞掉,搜索空的 catch、只打印字符串的 except 或忽略返回值的 C++ 调用;在边界补上操作名和输入标识,保留原始异常作为原因。排错的目标是恢复调用链,而不是把所有失败都改成 null

从原始输入到可组合函数

先让校验和转换函数接收值并返回结果,不在函数内部读环境、写文件或调用模型。上层负责副作用,核心函数就能在 JS/TS、Python 和 C++ 的测试中用固定数据独立运行。

function normalizeReading(raw) {
  if (!Number.isFinite(raw?.value)) return { ok: false, code: "invalid_value" };
  return { ok: true, value: Math.round(raw.value * 10) / 10 };
}
console.log(normalizeReading({ value: 21.46 }));

运行结果应为 { ok: true, value: 21.5 }。该函数没有隐含读写,因此可以从命令行、HTTP 路由或 ROS 消息回调调用。若要记录日志,把 logger 当作显式依赖传入,或由边界记录结果,不要为了打日志让每个纯函数都创建全局客户端。

验证组合关系与副作用

为纯函数测试正常值、NaN、空输入和临界精度;再为适配器单独测试文件不存在、网络超时等 IO 失败。组合测试只验证“解析 → 业务函数 → 输出”的顺序与错误映射。Python 模块导入时不应自动启动任务;C++ 头文件不应定义会执行副作用的全局对象;JS/TS 模块顶层也应避免连接数据库。

如果一个用例仍需要大量 mock 才能运行,说明依赖可能没有作为参数或小接口暴露。把核心函数的输入输出写成表格,再决定哪些真实适配器只需一个 fake;依赖越少,迁移到新语言时越容易保持相同行为。

当一个函数难以测试,先检查它是否同时读取全局配置、计算规则、写存储并格式化响应。把这些意图拆成可命名步骤,通常比给整个函数加 mock 更容易理解和维护。

迁移练习

01
TRY IT YOURSELF

把失败情况写出来

把 parse_port 改成:空字符串、非数字输入和超出 1 到 65535 范围的端口都返回明确错误,并说明 Python 与 C++ 模块各自暴露什么接口。

给我一点提示

先决定返回值结构,再写实现。例如 { ok: false, error: string };不要把范围检查散落在调用方。

查看参考答案
解析函数先 trim 并转换,失败返回 port must be an integer,超出范围返回 port must be in 1..65535。Python 可返回 Result 或抛出专用异常,C++ 可返回 expected<int, ParseError>;CLI 层统一转换为退出码。
本节结论

函数签名、模块边界和错误策略共同构成接口。语言迁移时,把隐含约定写出来,往往比一比一翻译语法更重要;可组合的核心函数应该不依赖终端、网络或当前时间。

小结

函数可组合的前提是依赖显式、职责单一、失败可理解。将副作用推到边缘后,同一规则可以由多个入口复用,也更容易进行语言迁移与回归测试。

当前学习阶段运行与表达
0/7

阶段共 7 节课,按顺序完成更容易建立完整的迁移模型。