【译】【OCaml函数式编程教程】第四章:递归

717 阅读3分钟

IV. 递归

1. 递归函数

您可能已经注意到,到目前为止我们还没有使用过 forwhile或其它循环。事实上,函数式语言中并不存在这种结构。为了多次重复同一个操作,我们会使用递归函数

递归函数的定义非常简单:这就是一个在给定时刻来调用自身的函数。一般来说,这类函数定义了两类情况(用if/else或者match):一个不会递归的基层情况,以及一个会递归地调用函数到“上一级”的递归情况。

如果忘记编写基层情况的话,会和Python中while(True)有相同的效果:函数会无休止地调用下去,您的程序永远不会停止。

recursivite-again.jpg

在OCaml中,需要在let之后使用关键词rec来表示这个函数是递归的。

举个例子,以下是一个递归地计算从0到 n 的和的函数:

(* 我们假定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

或者另一种图表:

lebochemadeclara.png

可以挑一个您喜欢的来看,重点是要理解这个函数是怎么进行计算的(如果您想的话,可以观察这两个图表)。一旦您知道了最主要的基础知识,您很容易就能理解所有的递归函数。

您需要的只是一次顿悟。如果您想要的话,这里还有一些递归函数的例子:

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), u(0)定义为常数(基层情况)。

递归函数可以做到其它编程语言中循环的所有功能,甚至是更多。有些排序算法可以递归地实现,某些函数甚至无法用其他方式编写。在 OCaml(以及许多其他函数式语言)中,递归总是用于操作列表。 这是一个非常强大的工具,但你必须花时间来理解它。

2. 递归类型

递归类型和递归函数有些相似,是一种在定义中包含了自身的类型。因此这也属于加法类型,其中有一个(或数个)constructor与相同类型的数据关联。同样的,我们也会定义一个不递归的基层情况。

我们也可以构建一个元素列表:基层情况为一个空列表,递归情况为一个元素后再加另一个列表。

以下例子为一个整数列表:

type list_entier =
  | Empty
  | Element of int * list_entier

(* 列表[1,2,3]可以写做: *)
let my_list =
  Element(
    1,
    Element(
      2,
      Element(
        3,
        Empty
      )
    )
  )

但我们也可以假设一种更复杂的情况,比如说一个包含intfloat的列表:

type list_numbers =
  | Empty
  | ElementInteger of int * list_numbers
  | ElementReal of float * list_numbers

递归类型可以用递归函数遍历,递归函数可以对constructor使用match

let sum_elements (list : list_integer) : int =
  match list with
  | Empty -> 0
  | Element(x, rest) -> x + (sum_elements rest)

关于递归类型没有更多要说的了。理解它们的最好方法是去练习使用它们。

3. 课后练习

在经过两小节的学习后,这里有一份测验来检测自己。

已知我们定义了以下类型:

type itinerary =
  | Arrive
  | Take of string * itinerary

type recipe =
  | Taste
  | Cook of int * recipe (* int代表烹饪时长 *)
  | Instruction of string * recipe
  | TakeIngredient of string * recipe

完成以下递归函数:

(* 给出一种可阅读的路线。
*
* 比如:
*
* 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 " ^ ______
<答案点我>
    Take
<答案点我>
    (display continue)
(* 记录菜谱的步骤数目 *)
let rec count_steps (r : recipe) : int =
  match r with
  | TakeIngredient(_, s) | Instruction(_, s) | Cook(_, s) -> ______
  | Taste -> 0 (* 品尝不算在步骤内 *)
<答案点我>
    1 + count_steps s 或 count_steps s + 1
(* 计算烹饪总时长,可能会有好几步 *)
let rec cook_total (r : recipe) : int =
  match r with
  | ______ -> 0
  | Cook(time, s) -> ______
  | Instruction(_, s) | TakeIngredient(_, s) -> cook_total s
<答案点我>
    Taste
<答案点我>
    time + (cook_total s) 或 (cook_total s) + time

我们想要编写一个递归类型来描述一个山洞,然后写出一个能找到出口的算法。我们有三种类型的“路”:

  • 死胡同;
  • 出口;
  • 岔路口,有一条向左的路和一条向右的路。

完成下列加法类型的定义。

type cave =
  | ______
  | ______
  | ______ of ______

(* 已知我们有这个函数: *)
let end_of_the_cave (c : cave) : bool =
  match c with
  | Impasse | Exit -> true
  | ForkRoad(_, _) -> false
<答案点我>
   Impasse
<答案点我>
    Exit
<答案点我>
    ForkRoad
<答案点我>
    cave * cave