DEATHTOUCH 发表于 2023-11-25 21:40

OCaml 学习笔记(Update 2023-12-27)

本帖最后由 DEATHTOUCH 于 2023-12-27 18:24 编辑

新开一坑,其实OCaml在之前就陆续学过了,现在再温习一遍顺便写一些笔记(兼简单入门教程)来加深印象。

作为一种函数式语言,和我们平常使用的命令式语言有很大的不同,即使很多函数式语言的优秀操作已经被广泛借鉴到了各种语言中。
学习一门函数式语言并不会给你搞到更多的钱,但是可以开阔自己的视野(当作一种兴趣爱好吧)。

闲话少说,下面是正文部分。

https://static.52pojie.cn/static/image/hrline/4.gif


# OCaml 学习笔记

该笔记主要是基于 [*OCaml Programming: Correct + Efficient + Beautiful*](https://cs3110.github.io/textbook/cover.html) 和 [*OCaml 语言编程基础教程*](https://book.douban.com/subject/30330930/) 这两本书,还有 [*Real World OCaml*](https://dev.realworldocaml.org/toc.html),以及一些官网的资料,留下的一些学习记录,顺序可能和这些书都不完全一样,内容也可能不会很全面,需要学习最好还是配合这几本书。

目前第一本书是只有英文的,是康奈尔大学的教材,比较适合入门;第二本则是国内作者编写的,也可以用作入门使用;而第三本书则是相当于高级篇,最新的是第二版的,中文只有第一版的,内容会更有深度。

# Part 1 - 环境配置与基本用法

## 1.1 - 环境配置

官网提供了基本的[安装教程](https://ocaml.org/install),照着做就行了,不过对于 Windows,我并不推荐使用 *Diskuv OCaml* 这个发行版,而是建议使用 WSL2 按照 Linux 的方法来,不过可能会遇到一些网络问题。

官方还提供了一个在 Windows 安装的[教程](https://ocaml.org/docs/ocaml-on-windows),按照目前(2023年11月)的内容,还未提供原生的 Windows 支持,估计 2024 年会提供这个支持。

如果按照官网的教程在 WSL2 或 Linux 安装出现问题,建议改善网络条件,或者可以通过 Linux 发行版的包管理器安装一个 *opam*,例如在 Debian 系可以使用 `sudo apt install opam` 来进行安装,安装之后首先需要使用 `opam init`执行初始化命令(记得允许添加到 PATH),之后则可以通过 `opam switch create xxx` 来创建一个编译器,其中的 “xxx” 是版本号,具体可以通过 `opam switch list-available` 查看,建议 *ocaml-base-compiler* 字段的版本号。比如 `opam switch create 5.1.0` 就会下载并安装一个 5.1.0 的 OCaml 编译器,这个过程比较慢,需要等一段时间。

比如这是一次安装的输出内容:

```
<><> Installing new switch packages <><><><><><><><><><><><><><><><><><><><><><>
Switch invariant: ["ocaml-base-compiler" {= "5.1.0"} | "ocaml-system" {= "5.1.0"}]

<><> Processing actions <><><><><><><><><><><><><><><><><><><><><><><><><><><><>
∗ installed base-bigarray.base
∗ installed base-threads.base
∗ installed base-unix.base
∗ installed ocaml-options-vanilla.1
⬇ retrieved ocaml-base-compiler.5.1.0(https://opam.ocaml.org/cache)
∗ installed ocaml-base-compiler.5.1.0
∗ installed ocaml-config.3
∗ installed ocaml.5.1.0
∗ installed base-domains.base
∗ installed base-nnp.base
Done.
# Run eval $(opam env --switch=5.1.0) to update the current shell environment
```

记得按照最后一行的内容执行一下 `eval $(opam env)` 命令。

如果安装了多个编译器,可以通过 `opam switch list` 命令(或者直接 `opam switch`,因为默认就是)查看,通过 `opam switch set xxx` 切换一个版本。

之后按照官网,使用

```bash
opam install dune merlin ocaml-lsp-server odoc ocamlformat utop dune-release
```

来安装必要的工具,其中需要注意的是 *utop* 和 *dune*,这俩在之后是一直要用的,其他的主要在编辑器用到的多。其中 utop 是 REPL 的一个顶层实现,类似于 python 的交互式环境。其实直接在终端执行 ocaml 就是基础的顶层,不过 utop 明显更加强大。不过所有的开发都在顶层是不现实的,因此需要使用构建系统 dune 来进行开发。

注意上述操作对于每一个 switch 都是独立的,因为这些工具的管理器 opam 是基于源码的,所以需要根据当前编译器进行编译。一般每个大版本保留一个最新发布的稳定版编译器就行了,比如目前的 5.1.x 版本以及稍早的 4.14.x 版本(当然学习来说最新的就可以了)。

对于编辑器,可以使用 *Vim* 和 *Emacs* 这种传统的,也可以用这些年最流行的 *Visual Studio Code*。对于 Windows 环境,建议使用 vscode 搭配 WSL 插件,并在虚拟机安装 *OCaml Platform* 插件来使用。

## 1.2 - 基本使用

直接在终端运行 utop 即可进入顶层,如果提示没有安装请检查 opam 是否安装成功。

输入一些基本的加减乘除运算,在最后使用两个连续的分号再按下回车就会执行。例如 `1 + 1;;` 就是一句有效的算术命令。

注意顶层的每一个输入都需要以**两个分号**结尾,表示这是一个表达式。退出顶层的方法则是输入 `#quit;;` 并回车。

对于使用文件的操作,可以新建一个叫 “hello.ml” 的文件并输入如下内容:

```ocaml
let _ = print_endline "Hello, World!"
```

这里的 `let _` 也可以换成 `let ()`,效果是一样的(当然原理不一样,在 utop 试试看区别)。

使用命令 `ocaml hello.ml` 即可运行并输出指定的字符串。

注意在文件写的代码不需要在结尾和语句之间加两个分号的,那是顶层特定的要求。

还可以使用 dune 工具创建一个项目,例如使用命令 `dune init project hello` 就会在当前目录创建一个子目录 “hello” 并在内部生成一系列的文件。使用 `tree hello` 命令可以查看目录结构:

```
hello
├── bin
│   ├── dune
│   └── main.ml
├── dune-project
├── hello.opam
├── lib
│   └── dune
└── test
    ├── dune
    └── hello.ml
```

其中的源代码就放在了 “hello/bin/main.ml” 里面,默认就是一句打印 “Hello, World!” 的内容。

使用 `dune exec hello` 就可以构建并运行这个项目了。

# Part 2 - 语言基础知识

## 2.1 - 基本类型

### int 类型

在 utop 顶层输入任意整数加两个分号,就可以得到最基本的整数类型了。

```
utop # 52;;
- : int = 52
```

整数定义时可以用下划线来分割,从视觉上来好看一些:

```
utop # 1_000;;
- : int = 1000
utop # 5_2000;;
- : int = 52000
```

整数有加减乘除模这5种基本运算符:

```
utop # 1 + 1;;
- : int = 2
utop # 1 - 1;;
- : int = 0
utop # 2 * 2;;
- : int = 4
utop # 5 / 2;;
- : int = 2
utop # 5 mod 2;;
- : int = 1
```

这些和大多数语言都是类似的。

还可以通过 `max_int;;` 还有 `min_int;;` 获取整数的表示范围:

```
utop # max_int;;
- : int = 4611686018427387903
utop # min_int;;
- : int = -4611686018427387904
```

可以看到实际上的范围是 -2^62^ 到 2^62^ ,而不是一般认为的 2^63^ ,当然这都是对于 64 位来说的,对于 32 位则是 2^30^ 了。不过目前大多数使用的系统都是 64 位,所以通常就是上述的结果。

整数还可以使用二进制、八进制和十六进制表示,和其他很多语言是差不多的:

```
utop # 0b110100;;
- : int = 52
utop # 0o64;;
- : int = 52
utop # 0x34;;
- : int = 52
```

和很多语言一样,`0b 0o 0x` 和 `0B 0O 0X` 是一样的,也就是前缀不区分大小写。

除了上述默认的 `int` 类型,还有 `int32` 和 `int64` 这两个和标准的整数类型,使用方法是后缀 `l`(小写的L) 以及 `L`。

```
utop # 52l;;
- : int32 = 52l
utop # 52L;;
- : int64 = 52L
```

注意上述的三种整数类型不能直接混合使用,需要使用相应的函数转换,一般的原则是转换到宽度最大的。

```
utop # Int64.add 52L (Int64.of_int32 52l);;
- : int64 = 104L
utop # Int64.add 52L (Int64.of_int 52);;
- : int64 = 104L
```

这里涉及到函数调用,后面有说明。

### float 类型

浮点数的基本表示法如下:

```
utop # 52.;;
- : float = 52.
utop # 52.666;;
- : float = 52.666
utop # 5.2e1;;
- : float = 52.
utop # 52E0;;
- : float = 52.
```

运算和 int 差不多,但是需要在运算符后面加一个点(mod 则是需要调用函数 mod_float):

```
utop # 1. +. 1.;;
- : float = 2.
utop # 52. /. 10. ;;
- : float = 5.2
utop # mod_float 52. 11.5 ;;
- : float = 6.
```

混合整数和浮点数的运算需要函数转换,否则无法运行:

```
utop # float_of_int 52 /. 11.5 ;;
- : float = 4.52173913043478226
```

使用 `max_float` 和 `min_float` 可以查看浮点数的表示范围:

```
utop # max_float;;
- : float = 1.79769313486231571e+308
utop # min_float;;
- : float = 2.22507385850720138e-308
```

这也说明了浮点数在 OCaml 是双精度的。

### char 类型

OCaml 的 char 和 C 一样,都是只能取值 0 到 255 ,也就是 ASCII 和一些扩展字符。语法也和 C 一样用单引号。

```
utop # '5' ;;
- : char = '5'
utop # '2' ;;
- : char = '2'
```

可以用 `char_of_int` 看编码对应的字符:

```
utop # char_of_int 112;;
- : char = 'p'
utop # char_of_int 106;;
- : char = 'j'
utop # char_of_int 255;;
- : char = '\255'
utop # char_of_int 256;;
Exception: Invalid_argument "char_of_int".
```

同理还有 `int_of_char` 就不细说了。

### string 类型

字符串用双引号包裹,中文用 UTF-8 编码:

```
utop # "52pojie";;
- : string = "52pojie"
utop # "吾爱破解";;
- : string = "吾爱破解"
```

使用 `print_string` 可以打印一个字符串,使用 `print_endline` 还可以顺便加一个换行:

```
utop # print_string "52pojie";;
52pojie- : unit = ()
utop # print_endline "吾爱破解";;
吾爱破解
- : unit = ()
```

还有一些常用函数处理字符串:

```
utop # String.trim "52 pojie ";;
- : string = "52 pojie"
utop # String.length "吾爱破解";;
- : int = 12
utop # String.get "52pojie" 3;;
- : char = 'o'
utop # String.get "吾爱破解" 3;;
- : char = '\231'
utop # String.contains "52pojie" '2';;
- : bool = true
```

为了打印或者生成字符串,还有 `Printf` 模块的一系列函数:

```
utop # Printf.printf "%dpojie\n" 52;;
52pojie
- : unit = ()
utop # Printf.sprintf "52%s\n" "pojie";;
- : string = "52pojie\n"
```

### unit 类型

注意到前面 print_string 以及 printf 这些函数,其返回值是 `- : unit = ()` 而不是没有返回值。这是因为作为一种函数式语言,OCaml 需要任何函数都有一个返回值,但是一个输出字符串的函数用不着返回值,于是就定义了 unit 类型,相当于无返回。

```
utop # ();;
- : unit = ()
```

unit 类型只有一个值,也就是 `()` ,即一对括号。

unit 类型也可以作为函数的参数,比如:

```
utop # print_newline ();;

- : unit = ()
```

注意空号就是打印出了一个换行符。

### bool 类型

bool 类型和很多语言一样,只有 true 和 false 这两个值,同时支持一些基本的函数(运算符):

```
utop # not false;;
- : bool = true
utop # true || false;;
- : bool = true
utop # true && false;;
- : bool = false
```

当然还有一些基本的比较运算:

```
utop # 1 < 2;;
- : bool = true
utop # 1 = 2;;
- : bool = false
```

更多内容在之后的表达式部分。

## 2.2 - 表达式

之前基本类型的 int 和 float 类型中就使用了一些基本的表达式,稍微复杂点的就是将它们复合起来。

```
utop # 1 + 2 + 16;;
- : int = 19
utop # float_of_int 15 +. 2.5 +. 11.5;;
- : float = 29.
```

### let 表达式

let 表达式基本语法是 `let v = e` ,即将一个表达式 e 求值并绑定(bind)到变量 v 。

```
utop # let a = 52;;
val a : int = 52
```

类型推导系统得出了表达式 `52` 的类型是 int ,之后 a 就暂时代表了 52 这个值。

```
utop # let b = a + 1;;
val b : int = 53
```

由于 let 表达式是绑定机制,所以可以重新定义变量 a 。

```
utop # let a = 50;;
val a : int = 50
utop # b;;
- : int = 53
```

这不会影响到变量 b ,这是因为 b 在定义的时候已经求值了,不会受 a 的影响。

事实上 a 可以重新绑定到任意类型的值:

```
utop # let a = "52pojie";;
val a : string = "52pojie"
```

都不会有影响,这和大多数语言的赋值有本质的区别。

let 表达式还有并行定义的方法:

```
utop # let a = 1 and b = 2;;
val a : int = 1
val b : int = 2
```

但是下面是错误的:

```
utop # let x = 1 and y = x;;
Error: Unbound value x
```

因为并行定义是不能让 and 后面的表达式依赖之前未定义的变量。

下面的用法就不会出错:

```
utop # let x = 2;;
val x : int = 2
utop # let x = 1 and y = x;;
val x : int = 1
val y : int = 2
```

因为在执行 `let x = 1 and y = x` 的时候,x 已经有了一个值 2 。

let 表达式还能局部定义,语法为 `let v = e1 in e2` ,例如:

```
utop # let x = 2 in x + 2;;
- : int = 4
```

还可以嵌套使用:

```
utop # let a = 10 in let b = 20 in a + b;;
- : int = 30
```

其好处就是 let 表达式绑定的变量只能在局部使用,不会影响全局作用域,类似于局部变量的作用。

let 局部定义也可以并行定义:

```
utop # let a = 1 in let b = a and a = 2 in a + b;;
- : int = 3
```

这里还涉及到了局部作用域的概念,和大多数语言一样,局部定义同名变量会暂时遮蔽外部的变量,在离开作用域后效果就会消失。

### 比较运算符

```
utop # 1 < 2;;
- : bool = true
utop # 1. > 2.;;
- : bool = false
utop # 'a' = 'b';;
- : bool = false
utop # "abc" <> "abb";;
- : bool = true
```

和整数运算符 `+` 以及浮点是运算符 `+.` 这样有区分不同,比较运算符和类型无关,这是因为类型推断在比较运算符这里是不会有歧义的,而在算术运算那里是有歧义的。

相等和不相等是 `=` 和 `<>` ,和一些语言的 `==` 和 `!=` 不一样。其实在 OCaml 也支持 `==` 和 `!=` 的,但是有不同的作用。

`==` 和 `!=` 的意思有两个,一是对于非结构化数据,效果与 `=` 和 `<>` 一致;二是对于结构化数据,比较的是变量是否指向同一地址。

整数和字符是非结构化数据,浮点数和字符串是结构化数据。

比如:

```
utop # 52 = 52;;
- : bool = true
utop # 52 == 52;;
- : bool = true
utop # 'a' = 'a';;
- : bool = true
utop # 'a' == 'a';;
- : bool = true
utop # 1. == 1. ;;
- : bool = false
utop # 1. = 1.;;
- : bool = true
utop # "abc" = "abc";;
- : bool = true
utop # "abc" <> "abc";;
- : bool = false
utop # "abc" == "abc";;
- : bool = false
utop # "abc" != "abc";;
- : bool = true
```

### if 表达式

if 表达式的语法为 `if e1 then e2 else e3` ,其中表达式 e1 必须是 bool 类型的,e2 和 e3 必须是同一种类型的,此外 else 部分可选,前提是表达式 e2 是 unit 类型的,这也意味着大多数时候都是带有 else 分支的。

if 表达式和一般命令式语言的 if 表达式不一样,反而是和常见的三目运算符 `?:` 效果很相似。例如下面两条代码意思相同:

```cpp
int a = b > c ? d : e;
```

```ocaml
let a = if b > c then d else e
```

if 表达式还可以嵌套,类似于:

```ocaml
let a =
if b > c then d
else if b < c then e
else f
```

总的来说 if 表达式并没有复杂的语法,相对比较简单。

## 2.3 - 函数

### 简单函数

函数的定义非常简单,基本语法为 `let f x = e` 其中 f 是函数名,x 是参数,e 是表达式,比如说定义一个简单的加一函数:

```ocaml
let inc x = x + 1
```

```
utop # let inc x = x + 1;;
val inc : int -> int = <fun>
utop # inc 51;;
- : int = 52
```

函数的类型是用 `t1 -> t2 -> ... tn = <fun>` 来标注的,箭头的最后一个类型是返回值的类型,其余是参数列表的类型。

比如定义一个加法函数:

```
utop # let add a b = a + b;;
val add : int -> int -> int = <fun>
utop # add 51 1;;
- : int = 52
```

函数的调用方法非常简单,用空格区分每个参数就行了,如果参数存在函数调用需要括号:

```
utop # add 50 (inc 1);;
- : int = 52
```

这是因为函数的求值顺序是从左到右。

### 递归函数

定义递归函数的方法是在 let 后面加一个 rec 关键字:

```ocaml
let rec fact n =
if n = 0 then 1 else n * fact (n - 1)
```

这是一个简单的求阶乘函数。

```
utop # let rec fact n = if n = 0 then 1 else n * fact (n - 1);;
val fact : int -> int = <fun>
utop # fact 5 ;;
- : int = 120
utop # fact 10 ;;
- : int = 3628800
```

使用 `let rec ... and ...` 可以定义相互递归的函数:

```ocaml
let rec odd n =
n > 0 && even (n - 1)
and even n =
n = 0 || odd (n - 1)
```

```
utop #
let rec odd n =
n > 0 && even (n - 1)
and even n =
n = 0 || odd (n - 1);;
val odd : int -> bool = <fun>
val even : int -> bool = <fun>
utop # odd 5;;
- : bool = true
utop # even 5;;
- : bool = false
```

### 匿名函数

如果一个函数不绑定到一个名称,就是匿名函数了,匿名函数需要使用关键字 `fun` 来定义。

```
utop # fun x -> x + 1;;
- : int -> int = <fun>
```

匿名函数可以直接调用:

```
utop # (fun x -> x + 1) 51;;
- : int = 52
```

也可以把匿名函数绑定到一个名称,这和定义函数是一样的:

```
utop # let inc1 = fun x -> x + 1;;
val inc1 : int -> int = <fun>
utop # inc1 51;;
- : int = 52
```

### 管道操作

管道操作很适合连续调用单参数的简单函数,从而减少括号的数量。

定义平方函数 square 和自增函数 inc 如下:

```ocaml
let square x = x * x
let inc x = x + 1
```

假设需要按照如下顺序使用:

```ocaml
square (inc (square (inc 52)))
```

可以看到是非常繁琐的,但是使用管道操作就可以简化操作了:

```ocaml
52 |> inc |> square |> inc |> square
```

看起来就清晰很多了。

### 高阶函数

高阶函数就是一种输入参数或者返回值为函数的函数。

定义一个函数 f 如下:

```ocaml
let f g x = g x + x
```

该函数的参数 g 是一个函数,这个函数的类型通过 g x 推导出了其参数只有 1 个,又因为存在 + 运算符,可以得出 g x 的类型和 x 的类型都是 int ,所以得出函数 g 的类型应该是 `int -> int` ,于是函数 f 的类型为 `(int -> int) -> int -> int` 。

对于参数 g ,可以传入一个已有的函数:

```
utop # let double x = x * 2;;
val double : int -> int = <fun>
utop # f double 5;;
- : int = 15
```

也可以传入一个匿名函数:

```
utop # f (fun x -> x + 52) 52;;
- : int = 156
```

在 C 中类似的操作是将函数指针作为参数。

### 运算符

其实运算符本身就是一种特殊的函数,当运算符作为函数时,需要用一对括号包裹:

```
utop # (+);;
- : int -> int -> int = <fun>
```

但是注意乘法符号 `*` 加括号时需要留空格:

```
utop # ( * );;
- : int -> int -> int = <fun>
```

这是因为在 OCaml 里面,注释的形式是 `(* ... *)` 这样的。

在运算符周围加入括号可以将运算符当作函数调用:

```
utop # ( * ) 2 3;;
- : int = 6
```

而定义一个运算符的方法就是加括号:

```ocaml
let ( % ) a b = a mod b
```

这样就定义了百分号作为取余数的运算符。

```
utop # ( % );;
Error: Unbound value %
utop # let ( % ) a b = a mod b;;
val ( % ) : int -> int -> int = <fun>
utop # ( % );;
- : int -> int -> int = <fun>
utop # 52 % 10;;
- : int = 2
```

运算符可以超过一个符号,就像前面的管道操作一样。

```ocaml
let ( >>> ) x f = f x
```

这个函数的类型比较复杂( `'a -> ('a -> 'b) -> 'b = <fun>` ),涉及到多态函数。

使用之前定义的 inc 和 square 来测试一下:

```
utop # 2 >>> inc >>> square >>> inc;;
- : int = 10
```

### 多态函数

定义一个函数 add 如下:

```ocaml
let add a b = a + b
```

由于 `+` 运算符的参数类型都是 int ,所以可以得出 add 的两个参数的类型都是 int 。

但是如果定义一个这样的函数:

```ocaml
let id x = x
```

一个简单返回自身的函数,其类型则是 `'a -> 'a = <fun>` ,其中的 `'a` 就代表了多态类型,即可以是任意类型。如果类型较多,通常的顺序是 `'a 'b 'c` ,依次读作 `alpha beta gamma` ,如果还有更多就不清楚了,理论上可以无限多。

再看之前自己定义的管道运算函数的类型:

```
utop # let ( >>> ) x f = f x;;
val ( >>> ) : 'a -> ('a -> 'b) -> 'b = <fun>
```

参数 x 的类型是 `'a` ;参数 f 要求一个 `'a` 类型的参数,返回 `'b` 类型的结果;整个函数的结果就是 `'b` 类型的。因为对于 `f x` 调用,不能确定结果是不是和 x 一样,所以就会有另一个类型。

实际上自带的管道操作 `|>` 其类型和自行定义的 `>>>` 是一样的。

```
utop # 52.5 |> int_of_float |> char_of_int;;
- : char = '4'
```

这就是一个很好的类型变化的例子。

### 标签和可选参数

参数的名称可以带一个标签,这样可以通过标签名为其选择参数。

```ocaml
let f ~name1:a ~name2:b = a + b
```

可以这样调用:

```ocaml
utop # f ~name1:2 ~name2:50;;
- : int = 52
utop # f ~name2:50 ~name1:2;;
- : int = 52
```

如果标签和参数名一致,可以直接:

```ocaml
let f ~a ~b = a + b
```

标签参数一般不直接使用,因为需要手动指定每一个标签名,导致比较麻烦。

一般标签函数需要和可选函数搭配使用。

```ocaml
let f ?name1:(a=5) b = a + b
```

还可以简化为:

```ocaml
let f ?(a=5) b = a + b
```

调用时可以指定可选参数,如果不指定就是默认值:

```
utop # f ~a:10 10;;
- : int = 20
utop # f 10 ~a:10;;
- : int = 20
utop # f 10;;
- : int = 15
```

### 尾递归

考虑一个求斐波那契数列的函数(不考虑非正数的情况):

```ocaml
let rec fib n =
if n < 3 then 1
else fib (n - 1) + fib (n - 2)
```

如果传入的参数很大,比如达到了 40 甚至 50 ,或者更高,就会发现计算速度很慢。这是因为递归的开销很大,需要重复计算。

但是如果是尾递归的函数,就会被编译器优化为循环操作。尾递归函数的意思是函数递归调用时的结果不在栈上暂存。

比如上述 fib 函数可以尾递归化如下:

```ocaml
let fib_tr n =
let rec fib_aux n p q =
    if n = 1 then q
    else fib_aux (n - 1) q (p + q)
in
fib_aux n 0 1
```

测试较大参数的时候,结果也是秒出的(参数太大了整数先溢出了)。

这个函数的尾递归化原理较为复杂,下面详细分析一下。

在函数 fib_tr 内部定义一个辅助函数 fib_aux ,该函数的参数 n 仅用作计数,参数 p 表示前一个值,参数 q 表示当前的值。

按照斐波那契数列定义,下一个值是前一个值加上当前的值,于是在递归调用的时候将当前值作为前一个值,将下一个值作为当前值。

当 n 计数到 1 的时候,当前值就是数列的结果了。初值的选取则是根据 n = 1 的情况,选取为 1 ,而前一个值则选为 0 ,确保 n = 2 时相加结果也是 1 。


https://static.52pojie.cn/static/image/hrline/4.gif

电子书下载:OCaml 语言编程基础教程 和 Real World OCaml 中文版

页: [1]
查看完整版本: OCaml 学习笔记(Update 2023-12-27)