Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

OCaml 是一种支持函数式、命令式和面向对象编程范式的静态类型语言。

下载

建议在MacOS,Linux(WSL)系统上运行Ocaml。

依次运行以下命令即可下载Ocaml及其相关工具:

Linux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 1. 更新系统包索引
sudo apt update

# 2. 安装 opam(OCaml 的包管理器)
sudo apt install opam -y

# 3. 初始化 opam(首次运行会创建配置文件)
opam init -y --bare

# 4. 加载 opam 环境变量(将 ocaml 等命令加入 PATH)
eval $(opam env)

# 5. 创建指定版本的 OCaml 编译环境(这里是 4.14.1)
opam switch create 4.14.1

# 6. 再次加载 opam 环境(进入新 switch)
eval $(opam env)

# 7. 安装配套工具(dune 构建工具,utop REPL,ocaml-lsp-server 语言服务)
opam install -y dune utop ocaml-lsp-server

MacOS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 0. 安装 Homebrew(如果已经安装可以跳过)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew update

# 1. 安装 OCaml 和 OPAM(OCaml 的官方包管理工具)
brew install ocaml opam

# 2. 初始化 OPAM(第一次使用时必须)
opam init
eval $(opam env)

# 3. 创建一个新的 OCaml 编译器环境(可指定版本)
opam switch create 4.14.1
eval $(opam env)

# 4. 安装 utop(更好的 OCaml REPL 工具)
opam install utop
eval $(opam env)

# 5. 启动 utop 测试
utop

其中utop 是 OCaml 的一个增强型交互式命令行(REPL),提供语法高亮、自动补全和更友好的开发体验:

image-20250522163320084

在utop中:

  • # 表示Interpreter在等待输入(input);
  • ;; 用于标志一个表达式的结束,告诉解释器可以开始执行(跟C语言中的;一样);
  • - 则表示输出的结果

关于自动补全的部分,可以使用Alt+Left以及Alt+Right来选择,然后使用Alt+Down进行确认。(这里的Left, Right, Down指的是键盘上的方向键。)

在后续内容中我们会utop的格式来区分代码以及结果。

Basics

定义变量

使用let

1
2
3
4
5
# let seven = 3+4;;
val seven : int = 7

# seven;;
- : int = 7

变量名的开头需为小写字母。

也可以定义tuple:

1
2
3
let (x,y) = (3,4.0)

let (_,y) = (3,4)

_表示anaonymous variable

Records

在 OCaml 中,记录(records)是一种 静态、固定字段名 的数据结构,类似于 C 语言的 struct 或 Python 的类实例。

字段名和类型在定义时就固定了,不能动态添加或删除。

跟字典不一样。

1
2
3
4
5
6
7
8
9
10
type person = {given:string; sur:string; age:int}

let paul = { given="Paul"; sur="Meier"; age=24 }

let hans = { sur="kohl"; age=23; given="hans"}

let hansi = {age=23; sur="kohl"; given="hans"}

# hans = hansi;;
- : bool = true

利用component的名字来访问,比如说

1
2
# paul.given;;
- : string = "Paul"

或者利用pattern matching

1
2
3
4
# let {given = x;sur = y; age = z} = paul;;
val x : string = "Paul"
val ny : string = "Meier"
val z : int = 24
1
2
# let {given = x;_} = paul;;
val x : string = "Paul"

Case Distinction 分类讨论

使用matchif

1
2
3
4
5
match n 
with
| 0 -> "null"
| 1 -> "one"
| _ -> "uncountable!"
1
2
3
match e with
| true -> e1
| false -> e2

第二个也可以写成

1
if e then e1 else e2

可以把Ocaml里的match理解成一个分段函数,将不同的定义域上的值映射到其他值上。

也可以把match理解成if, else,其中_else

Lists 列表

使用[]::构造列表:

1
2
3
4
5
6
7
8
9
10
11
# let mt = [];;
val mt : 'a list = []

# let l1 = 1::mt;;
val l1 : int list = [1]

# let l = [1;2;3];;
val l : int list = [1; 2; 3]

# let l = 1::2::3::[];;
val l : int list = [1; 2; 3]

注意,列表里的所有元素都需要是同一个type的。

我们可以针对一个列表进行pattern matching:

1
2
3
4
5
6
# match l
with
[] -> -1
| x::xs -> x;;

-: int = 1

其中x指的是当前列表中的第一个元素,而xs是所有剩余的元素。

在 OCaml 里,::列表拆分(cons)运算符,模式的语法是:

1
head::tail

head部分默认是列表的第一个元素,tail则是剩余。(这里的变量名无关紧要,x::xs, head::tail的效果是完全一样的)

函数

同样使用let来定义函数:(所以在Ocaml里可以把函数理解成变量)

1
2
3
4
5
# let double x = 2 * x;;
val double : int -> int = <fun>

# (double 3, douoble (double 1));;
- : int * int = (6,4)

也可以写成

1
2
# let double = fun x -> 2*x;;
val double : int -> int = <fun>

fun来强调一下这里定义的是一个函数。

不过要注意一点,如果在定义函数时用到了一个之前定义的变量,那么在定义完函数之后修改这个变量并不会修改这个函数的操作。

1
2
3
4
5
6
7
8
9
10
11
# let factor = 2;;
val factor : int = 2

# let double x = factor * x;;
val double : int -> int = <fun>

# let factor = 4;;
val factor : int = 4

# double 3;;
- : int = 6

注意到这里的结果是6($2\cdot3$)而不是12($4\cdot3$)。

Recursive Functions 递归函数

使用rec

1
2
3
4
5
6
7
8
# let rec fac n = if n < 2 then 1 else n * fac (n-1);;
val fac : int -> int = <fun>

# let rec fib = fun x ->
if x <=1 then 1
else fib (x-1) + fib (x-2);;

val fib : int -> int = <fun>

想要几个新定义的函数交叉引用递归的话则需要用and连接:

1
2
3
4
5
6
7
8
9
# let rec even n = 
if n=0 then "even"
else odd (n-1)
and odd n =
if n=0 then "odd"
else ecen (n-1);;

val even : int -> string = <fun>
val odd : int -> string = <fun>

使用Case Distinction来定义函数

1
2
3
4
5
6
7
8
# let rec length = fun l -> match l with
| [] -> 0
| x:xs -> 1 + length xs;;

val length : 'a list -> int = <fun>

# length [1;2;3];;
- : int = 3

也可以写成

1
2
3
4
5
6
7
8
# let rec length = function
| [] -> 0
| x:xs -> 1 + length xs;;

val length : 'a list -> int = <fun>

# length [1;2;3];;
- : int = 3

Local Definitions

可以使用let或者let rec定义一个局部变量(不过需要配合in一起使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
# let x = 5 in
let sq = x*x in
sq + sq;;

: int = 50

# let facin n =
let rec iter m yet =
if m > n then yet
else iter (m+1) (m*yet) in
iter 2 1;;

val facit : int -> int = <fun>

在facin的例子里,我们相当于在定义facin这个函数时定义并调用了一个新函数(iter)。

Tail Recursive 尾递归

我们拿阶乘来举例子,正常的是这么写递归:

1
2
3
let rec fac n =
if n <= 1 then 1
else n * fac (n-1);;

这样子的计算逻辑便是:

1
2
3
4
fac 5
= 5 * fac 4
= 5 * (4 * fac 3)
= ...

每次都需要递归下去直到得到fac 1的结果了之后再一层一层地乘回来,这样会导致所需的栈空间非常大。

而尾递归的写法则是:

1
2
3
4
5
6
let fac x =
let rec facit n acc =
if n <= 1 then acc
else facit (n - 1) (n * acc)
in
facit x 1

这样的流程则变成了:

1
facit 5 1  → facit 4 5  → facit 3 20  → facit 2 60  → facit 1 120  → 返回 120

相当于是一条路走下去。

用汇编的角度来理解就是,普通的情况需要一直call,但是尾递归的写法就只需要jump了。

常用函数

List.fold_left

语法:

1
List.fold_left f acc lst
  • f:是一个“累积函数”,定义如何处理当前元素和累积值。
    它的类型是 ('a -> 'b -> 'a),即接收当前累积值和当前元素,返回新的累积值。

  • acc:是初始累积值。

  • lst:是要处理的列表。

  • 从列表的左边开始,依次用 f 更新累积值。

List.filter

语法:

1
List.filter (fun x -> condition) list
  • List.filter:用于从列表中挑选符合条件的元素
  • 参数:
    • fun x -> condition:一个函数,对列表中的每个元素 x 判断是否满足条件(condition 是布尔表达式)。
    • list:要过滤的列表。
  • 返回值:一个新列表,包含原列表中所有满足条件的元素。