C4
p---
theme: smartblue
highlight: arduino-light
---
# IV. 递归
## 1. 递归函数
您可能已经注意到,到目前为止我们还没有使用过 `for`、`while`或其它循环。事实上,函数式语言中并不存在这种结构。为了多次重复同一个操作,我们会使用**递归函数**。
递归函数的定义非常简单:这就是一个在给定时刻来调用自身的函数。一般来说,这类函数定义了两类情况(用`if`/`else`或者`match`):一个不会递归的基层情况,以及一个会递归地调用函数到“上一级”的递归情况。
如果忘记编写基层情况的话,会和Python中`while(True)`有相同的效果:函数会无休止地调用下去,您的程序永远不会停止。
![recursivite-again.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8e7403006ab4c4380d6eb8dc8dddee9~tplv-k3u1fbpfcp-watermark.image)
在OCaml中,需要在`let`之后使用关键词`rec`来表示这个函数是递归的。
举个例子,以下是一个递归地计算从0到 *n* 的和的函数:
```ocaml
(* 我们假定n不为复数即使int类型已经这样规定了 *)
let rec sum (n : int) : int =
if n = 0 then
0
else
n + (sum (n - 1))
```
在这个例子中:
- 基层情况为`0`,当n = 0时;
- 迭代情况为`n + (sum (n - 1))`;
为了理解在n = 3时怎样运行,我们可以观察一下图表:
![recurs1.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1f57ade7e2a243a995aa87f2f5333461~tplv-k3u1fbpfcp-watermark.image)
或者另一种图表:
![lebochemadeclara.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d270105b0d2844728e34f3f545c8941f~tplv-k3u1fbpfcp-watermark.image)
可以挑一个您喜欢的来看,重点是要理解这个函数是怎么进行计算的(如果您想的话,可以观察这两个图表)。一旦您知道了最主要的基础知识,您很容易就能理解所有的递归函数。
您需要的只是一次顿悟。如果您想要的话,这里还有一些递归函数的例子:
```ocaml
let rec factorial n =
if n < 2 then
1
else
n * (factorial (n - 1))
(* 如果你不知道什么是斐波那契数列的话:
https://zh.wikipedia.org/wiki/%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0 *)
let rec fibonacci n =
if n < 2 then
1
else
(fibonacci (n - 1)) + (fibonacci (n - 2))
```
一个有助于理解递归数列概念的类比:为了计算 *u(n)*,我们总是需要 *u(n-1)*,所以我们需要计算所有的数值直到 *u(0)*,qui est défini de manière constante (le cas de base).
递归函数可以做到其它编程语言中循环的所有作用,甚至是更多。有些排序算法可以递归地实现,某些函数甚至无法用其他方式编写。在 OCaml(以及许多其他函数式语言)中,递归总是用于操作列表。
这是一个非常强大的工具,但你必须花时间来理解它。
## 2. 递归类型
递归类型和递归函数有些相似,是一种在定义中包含了自身的类型。因此这也属于加法类型,其中有一个(或数个)constructor与相同类型的数据关联。同样的,我们也会定义一个不递归的基层情况。
我们也可以构建一个元素列表:基层情况为一个空列表,递归情况为一个元素后再加另一个列表。
以下例子为一个整数列表:
```ocaml
type list_entier =
| Empty
| Element of int * list_entier
(* 列表[1,2,3]可以写做: *)
let my_list =
Element(
1,
Element(
2,
Element(
3,
Empty
)
)
)
```
但我们也可以假设一种更复杂的情况,比如说一个包含`int`或`float`的列表:
```ocaml
type list_numbers =
| Empty
| ElementEntier of int * list_numbers
| ElementReel of float * list_numbers
```
递归类型可以用递归函数遍历,递归函数可以对constructor使用`match`:
```ocaml
let sum_elements (list : list_entier) : int =
match list with
| Empty -> 0
| Element(x, rest) -> x + (sum_elements rest)
```
关于递归类型没有更多要说的了。理解它们的最好方法是去练习使用它们。
## 3. 课后练习
在经过两小节的学习后,这里有一份测验来检测自己。
已知我们定义了以下类型:
```ocaml
type itinerary =
| Arrive
| Take of string * itinerary
type recipe =
| Taste
| Cook of int * recipe (* int代表烹饪时长 *)
| Instruction of string * recipe
| TakeIngredient of string * recipe
```
完成以下递归函数:
```ocaml
(* 给出一种可阅读的路线。
*
* 比如:
*
* display(Take("rue of Mathematics", Take("road of Physique", Arrive)))
* = "Take the road of Mathematics, then take the road of Physique, then you have arrived."
*)
let rec display(i : itinerary) : string =
match i with
| Arrive -> "you have arrived."
| ______(name_road,continue) -> "take the " ^ name_road ^ ", then " ^ ______
```
<details>
<summary><答案点我></summary>
```ocaml
Take
```
</details>
<details>
<summary><答案点我></summary>
```ocaml
(display continue)
```
</details>
```ocaml
(* 记录菜谱的步骤数目 *)
let rec count_steps (r : recipe) : int =
match r with
| TakeIngredient(_, s) | Instruction(_, s) | Cook(_, s) -> ______
| Taste -> 0 (* 品尝不算在步骤内 *)
```
<details>
<summary><答案点我></summary>
```ocaml
1 + count_steps s 或 count_steps s + 1
```
</details>
```ocaml
(* 计算烹饪总时长,可能会有好几步 *)
let rec cook_total (r : recipe) : int =
match r with
| ______ -> 0
| Cook(time, s) -> ______
| Instruction(_, s) | TakeIngredient(_, s) -> cook_total s
```
<details>
<summary><答案点我></summary>
```ocaml
Taste
```
</details>
<details>
<summary><答案点我></summary>
```ocaml
time + (cook_total s) 或 (cook_total s) + time
```
</details>
我们想要编写一个递归类型来描述一个山洞,然后写出一个能找到出口的算法。我们有三种类型的“路”:
- 死胡同;
- 出口;
- 岔路口,有一条向左的路和一条向右的路。
完成下列加法类型的定义。
```ocaml
type cave =
| ______
| ______
| ______ of ______
(* 已知我们有这个函数: *)
let end_of_the_cave (c : cave) : bool =
match c with
| Impasse | Exit -> true
| ForkRoad(_, _) -> false
```
<details>
<summary><答案点我></summary>
```ocaml
Impasse
```
</details>
<details>
<summary><答案点我></summary>
```ocaml
Exit
```
</details>
<details>
<summary><答案点我></summary>
```ocaml
ForkRoad
```
</details>
<details>
<summary><答案点我></summary>
```ocaml
cave * cave
```
</details>