02 · 表达式与绑定:让每一步产生一个值
目标:用不可变绑定和条件表达式计算学习时长,并解释整数与浮点数的区别。
从 Go 的 if 语句转过来,先换一个观察角度:OCaml 的 if 本身产生结果,再由 let 给结果起名。你可以借用 Python 条件表达式、JS 三元表达式的经验理解这一点。先问“这个表达式的值是什么”,通常比寻找 return 更有帮助。
let minutes = 25 建立名字与值的绑定。再次写 let minutes = 30 会遮蔽旧名字,不会把旧值变成 30。函数内部使用 let x = ... in ...:in 后面的表达式可以使用 x,整个结构的结果就是后一个表达式的结果。
这里要区分绑定与对象内容。JS 的 const 也能帮助理解“名字不能重新赋值,但所指对象可能可变”;OCaml 是否允许修改内容同样取决于数据类型,本课程的普通 record 与 list 默认不可变,第 10 课的 ref 和 array 则可变。不要把 OCaml let 理解成 JS let 或 Python 的普通重新赋值。MDN:const 的绑定与对象内容
两个 if 分支必须产生相同类型。如果一边是数字而另一边是字符串,编译器无法给整个 if 一个一致的类型。整数运算使用 +、/,浮点运算使用 +.、/.;整数除法会截去小数部分,转换必须显式完成。
实验:同一个总时长的两种表示
创建 expressions.ml。Printf.printf 按格式打印:%d 是整数,%.2f 是保留两位小数的浮点数,%s 是字符串,\n 是换行。
let morning = 25
let evening = 35
let total = morning + evening
let hours = float_of_int total /. 60.0
let label =
let target = 60 in
if total >= target then "met" else "keep going"
let () =
Printf.printf "total=%d hours=%.2f %s\n" total hours label;
Printf.printf "integer hours=%d\n" (25 / 60)
先预测第二行会不会是 0.42,再在练习根目录运行:
ocaml expressions.ml
预期输出:
total=60 hours=1.00 met
integer hours=0
total 是两个整数相加;只有 float_of_int total 才把它转成浮点数。target 只在计算 label 的表达式内可见。第一行打印完返回 (),分号再启动第二次打印。
这里的 = 可以出现在绑定里,也可以作结构相等比较,例如 total = 60 的结果是 bool。<> 表示不相等。不要把其他语言里惯用的 == 搬过来;OCaml 的 == 表示物理相等,判断一般业务值优先用 =。
让输出反映一次真实改动
- 将
evening改为 20,总时长应为 45。 - 先写下条件分支的预测,再运行。第一行应为
total=45 hours=0.75 keep going。 - 在
label之后增加let evening = 100并重跑。已经算出的total不变;新绑定只影响后续代码。
独立练习:保持总时长 45,增加 remaining,表示距离 60 分钟还差多少;超过目标时返回 0。打印 remaining=15,再用总时长 70 检查它不会变成负数。
参考实现与一个类型判断
在打印前增加以下完整定义,并将下面的打印语句加入 let () 的末尾,前一条打印后保留分号:
let remaining = if total >= 60 then 0 else 60 - total
Printf.printf "remaining=%d\n" remaining
这是对主示例的局部修改,两个代码块不能脱离 total 独立运行。分别把 morning、evening 设为 25、20 和 35、35,结果应为 15 和 0。
if total >= 60 then 0 else "15" 不成立,因为 int 与 string 不一致。解决方法是决定你需要数值用于计算,还是字符串用于显示,而非强行绕过检查。
遇到 “expected int” 时,检查运算符和实参是不是都属于整数;遇到 Unbound value target,检查是否越过了 in 的作用域。不要在每个定义后随意加入分号:顶层 let 定义之间无需分号,分号用于依次求值的表达式。