OCaml 是一种支持函数式、命令式和面向对象编程范式的静态类型语言。
下载
建议在MacOS,Linux(WSL)系统上运行Ocaml。
依次运行以下命令即可下载Ocaml及其相关工具:
Linux:
1 | # 1. 更新系统包索引 |
MacOS:
1 | # 0. 安装 Homebrew(如果已经安装可以跳过) |
其中utop
是 OCaml 的一个增强型交互式命令行(REPL),提供语法高亮、自动补全和更友好的开发体验:
在utop中:
#
表示Interpreter在等待输入(input);;;
用于标志一个表达式的结束,告诉解释器可以开始执行(跟C语言中的;
一样);-
则表示输出的结果
关于自动补全的部分,可以使用Alt+Left
以及Alt+Right
来选择,然后使用Alt+Down
进行确认。(这里的Left
, Right
, Down
指的是键盘上的方向键。)
在后续内容中我们会utop的格式来区分代码以及结果。
Basics
定义变量
使用let
:
1 | # let seven = 3+4;; |
变量名的开头需为小写字母。
也可以定义tuple:
1 | let (x,y) = (3,4.0) |
_
表示anaonymous variable。
Records
在 OCaml 中,记录(records)是一种 静态、固定字段名 的数据结构,类似于 C 语言的 struct
或 Python 的类实例。
字段名和类型在定义时就固定了,不能动态添加或删除。
跟字典不一样。
1 | type person = {given:string; sur:string; age:int} |
利用component的名字来访问,比如说
1 | # paul.given;; |
或者利用pattern matching
1 | # let {given = x;sur = y; age = z} = paul;; |
1 | # let {given = x;_} = paul;; |
Case Distinction 分类讨论
使用match
和if
:
1 | match n |
1 | match e with |
第二个也可以写成
1 | if e then e1 else e2 |
可以把Ocaml里的match
理解成一个分段函数,将不同的定义域上的值映射到其他值上。
也可以把match
理解成if, else
,其中_
为else
。
Lists 列表
使用[]
和::
构造列表:
1 | # let mt = [];; |
注意,列表里的所有元素都需要是同一个type的。
我们可以针对一个列表进行pattern matching:
1 | # match l |
其中x指的是当前列表中的第一个元素,而xs是所有剩余的元素。
在 OCaml 里,::
是 列表拆分(cons)运算符,模式的语法是:
1 | head::tail |
head部分默认是列表的第一个元素,tail则是剩余。(这里的变量名无关紧要,x::xs
, head::tail
的效果是完全一样的)
函数
同样使用let
来定义函数:(所以在Ocaml里可以把函数理解成变量)
1 | # let double x = 2 * x;; |
也可以写成
1 | # let double = fun x -> 2*x;; |
用fun
来强调一下这里定义的是一个函数。
不过要注意一点,如果在定义函数时用到了一个之前定义的变量,那么在定义完函数之后修改这个变量并不会修改这个函数的操作。
1 | # let factor = 2;; |
注意到这里的结果是6($2\cdot3$)而不是12($4\cdot3$)。
Recursive Functions 递归函数
使用rec
:
1 | # let rec fac n = if n < 2 then 1 else n * fac (n-1);; |
想要几个新定义的函数交叉引用递归的话则需要用and
连接:
1 | # let rec even n = |
使用Case Distinction来定义函数
1 | # let rec length = fun l -> match l with |
也可以写成
1 | # let rec length = function |
Local Definitions
可以使用let
或者let rec
定义一个局部变量(不过需要配合in
一起使用):
1 | # let x = 5 in |
在facin的例子里,我们相当于在定义facin这个函数时定义并调用了一个新函数(iter
)。
Tail Recursive 尾递归
我们拿阶乘来举例子,正常的是这么写递归:
1 | let rec fac n = |
这样子的计算逻辑便是:
1 | fac 5 |
每次都需要递归下去直到得到fac 1
的结果了之后再一层一层地乘回来,这样会导致所需的栈空间非常大。
而尾递归的写法则是:
1 | let fac x = |
这样的流程则变成了:
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
:要过滤的列表。
- 返回值:一个新列表,包含原列表中所有满足条件的元素。