cv.cm

编码 · 编程语言

OCaml 入门教程

阅读约 8 分钟

简短回答

OCaml 是 ML 家族的静态类型函数式语言。编译器会为你推断类型,在程序运行前发现许多错误,并生成高速的原生代码。程序由不可变值、变体(带标签的联合)、模式匹配和模块构成。用 opam 安装它,用 ocaml hello.ml 运行文件,或用 ocamlopt 编译。

OCaml 是什么,谁在用

OCaml 诞生于法国的 Inria,现在由开源社区开发,参与者包括 Jane Street 和 Tarides 等公司。它把函数式编程与可选的命令式特性(可变引用、数组、循环)以及大多数代码用不到的对象系统结合在一起。OCaml 5 通过 domain 和效应处理器(effect handler)加入了共享内存并行。

OCaml 用于编译器和程序分析工具、Rocq(原名 Coq)这样的证明助手、交易系统(Jane Street 是最知名的工业用户),以及 Meta 的 Flow 和 Infer 等开发者工具。它也是教授函数式编程和类型系统的常用语言。

安装 OCaml 并运行 Hello, world

安装 OCaml 的标准方式是使用 OCaml 的包管理器 opam。装好 opam 后,运行一次 opam init,它会配置好编译器。然后用 opam install dune utop 安装构建工具 dune 和更友好的 REPL utop。

(* hello.ml *)
let () = print_endline "Hello, world!"
ocaml hello.ml            # run with the interpreter
ocamlopt -o hello hello.ml
./hello                   # native executable
# Hello, world!

let () = ... 是编写入口点的常用方式:它对表达式求值,并检查其返回 unit(无值)。在 REPL(utop 或 ocaml)中,每条语句以 ;; 结尾;在源文件中则不需要 ;;。对于真实项目,dune init project myapp 会创建项目,dune build 则编译它。

值与基本类型

类型示例说明
int42在 64 位系统上为 63 位;运算符 + - * / mod
float3.14有自己的运算符:+. -. *. /.
string"hello"不可变字节串;用 ^ 连接
char'a'单个字节
booltrue, false&&、||、not
unit()没有结果的表达式的值
let name = "Ana"
let age = 30
let height = 1.68 *. 100.0
let () = Printf.printf "%s is %d, %.0f cm\n" name age height
(* Ana is 30, 168 cm *)
let count = ref 0
let () = incr count; Printf.printf "count = %d\n" !count

绑定是不可变的。ref 是可变单元:!r 读取,r := v 写入。OCaml 从不在 int 和 float 之间自动转换;请使用 float_of_int 和 int_of_float。

控制流与函数

if 是表达式。函数用 let 定义;递归函数需要 let rec。参数用空格分隔,每个函数都是柯里化的,所以部分应用没有额外开销。

let sign n =
  if n > 0 then "positive"
  else if n < 0 then "negative"
  else "zero"
let rec factorial n = if n <= 1 then 1 else n * factorial (n - 1)
let add x y = x + y          (* int -> int -> int *)
let add5 = add 5
let () =
  for i = 1 to 3 do
    Printf.printf "%d squared is %d\n" i (i * i)
  done;
  Printf.printf "%s %d %d\n" (sign (-2)) (factorial 5) (add5 10)
(* negative 120 15 *)

注意 sign (-2) 中的括号:没有括号的话,OCaml 会把 sign - 2 读成减法。使用 ; 把返回 unit 的表达式依次串联起来。

列表、数组、元组与记录

结构字面量说明
列表[1; 2; 3]不可变单向链表;x :: xs 在前端添加
数组[| 1; 2; 3 |]大小固定,可变,用 a.(0) 索引
元组("Ana", 30)固定数量、类型可不同的一组值
记录{ name = "Ana"; age = 30 }字段有名字,需要先声明类型
Hashtbl / MapHashtbl.create 16可变哈希表 / 不可变的平衡树映射
type person = { name : string; age : int }
let people = [ { name = "Ana"; age = 30 }; { name = "Ben"; age = 17 } ]
let () =
  people
  |> List.filter (fun p -> p.age >= 18)
  |> List.map (fun p -> p.name)
  |> List.iter print_endline
(* Ana *)
let older = { (List.hd people) with age = 31 }

管道运算符 x |> f 的意思是 f x。List 模块提供 map、filter、fold_left、find_opt 等大量函数。

标志性特性:变体、模式匹配与模块

变体类型列出一个值所有可能的分支。模式匹配按分支拆解值,缺少分支时编译器会发出警告,这让重构更安全。

type shape =
  | Circle of float
  | Rectangle of float * float
let area = function
  | Circle r -> Float.pi *. r *. r
  | Rectangle (w, h) -> w *. h
let () =
  List.iter (fun s -> Printf.printf "%.2f\n" (area s))
    [ Circle 1.0; Rectangle (2.0, 3.0) ]
(* 3.14 then 6.00 *)
module StringMap = Map.Make (String)
let ages = StringMap.(empty |> add "Ana" 30 |> add "Ben" 17)
let () = Printf.printf "%d\n" (StringMap.find "Ana" ages)   (* 30 *)

模块把类型和函数组织在一起,每个文件就是一个模块(list.ml 对应 List)。函子(functor)是从模块到模块的函数:Map.Make (String) 会构建一个键为字符串的映射模块。签名(.mli 文件)用来隐藏实现细节。

错误处理

OCaml 有三种工具:option 表示可能缺失的值,result 表示可能失败并带有信息的操作,异常则用于异常情况。

let parse_age s =
  match int_of_string_opt s with
  | Some n when n >= 0 -> Ok n
  | Some _ -> Error "Age must not be negative"
  | None -> Error "Not a number"
let () =
  match parse_age "abc" with
  | Ok n -> Printf.printf "Age %d\n" n
  | Error msg -> print_endline ("Error: " ^ msg)
(* Error: Not a number *)
exception Empty_input
let first = function [] -> raise Empty_input | x :: _ -> x
let () =
  try print_int (first []) with Empty_input -> print_endline "empty list"

一个完整的小程序:词频统计

let () =
  let text = "the cat and the hat and the bat" in
  let counts = Hashtbl.create 16 in
  String.split_on_char ' ' text
  |> List.iter (fun w ->
         let n = Option.value (Hashtbl.find_opt counts w) ~default:0 in
         Hashtbl.replace counts w (n + 1));
  Hashtbl.to_seq counts
  |> List.of_seq
  |> List.sort (fun (_, a) (_, b) -> compare b a)
  |> List.filteri (fun i _ -> i < 2)
  |> List.iter (fun (w, n) -> Printf.printf "%s %d\n" w n)
(* the 3
   and 2 *)

程序用可变哈希表统计单词,把表转成 (word, count) 对的列表,按次数降序排序,并打印前两项。~default:0 是带标签的参数。

接下来读什么

  • ocaml.org:安装指南、“A Tour of OCaml”以及 Docs 下的其他教程。
  • OCaml 手册,包含完整的语言和标准库参考。
  • 学习用 dune 构建项目,用 opam 安装库。
  • 试试免费在线图书《Real World OCaml》,里面有更大的实用示例。

常见问题

OCaml 难学吗?

语法不多,但如果你来自命令式语言,用类型、递归和模式匹配来思考需要练习。类型检查器给出的错误信息很快就会变得容易读懂。

为什么 OCaml 的浮点数有单独的运算符?

OCaml 不重载算术运算符,所以 + 只能用于 int,+. 只能用于 float。这让类型推断保持简单、可预测。

OCaml 和 F# 有什么区别?

F# 派生自 OCaml,与它共用大部分核心语法,但 F# 运行在 .NET 上并采用有语义的缩进,而 OCaml 编译为原生代码或字节码,并且拥有带函子、更强大的模块系统。

OCaml 中什么时候需要 ;;?

只在交互式顶层(ocaml 或 utop)中需要,它告诉 REPL 对你输入的内容求值。在 .ml 源文件中通常省略。

全部编码教程