12.2.1 递归的序列表达式

来源:互联网 发布:电信网络资源管理系统 编辑:程序博客网 时间:2024/06/06 09:05
 

12.2.1 递归的序列表达式

 

    函数式编程中主要的控制流结构是递归。我们已经在很多例子使用过,写普通的函数,能够解决命令式编程中的循环问题,而不需依赖可变状态。当我们想写一个简单的递归函数时,使用 let rec 关键字,允许函数以递归方式调用自身。

    用于组合序列的 yield! 结构,也可以在序列表达式中执行递归调用,所以,我们可以使用相同的函数编程技术生成序列。清单 12.4 生成所有的小于 1 百万的阶乘,就像清单 12.1 的 C# 示例一样。

 

Listing 12.4 Generating factorials using sequence expressions (F# Interactive)

 

> let rec factorialsUtil(num, factorial) =
     seq { if (factorial < 1000000) then
                  yield sprintf "%d! = %d" num factorial
                  let num = num + 1
                  yield! factorialsUtil(num, factorial * num) };;
val factorialsUtil : int * int -> seq<string>

> let factorials = factorialsUtil(0, 1)
val factorials : seq<string> =
  seq ["0! = 1"; "1! = 1"; "2! = 2"; "3! = 6"; "4! = 24 ...]

 

    清单 12.4 以一个工具函数开始,取一个数字和其阶乘作为参数值。当我们在后面的代码中,想要计算阶乘序列时,就调用此函数,并给它一个最小数字,定义阶乘的起始序列。这里是零,因为,按照定义,零的阶乘是一。

    整个函数体就是一个 seq 块,因此,该函数将返回一个序列。在序列表达式中,我们首先检查最后的阶乘是否小于 1 百万,如果不是,就终止序列。if 表达式的 else 分支省略,所以,它不会产生任何附加的数字。如果条件为值,首先产生一个结果,它指示下一个阶乘被格式化为字符串。接下来,递增这个数,并执行递归调用。将返回一个从下一个数字开始的阶乘的序列,我们用 yield! 把它与当前的序列组合起来。

    注意,因为 C# 没有相当于 yield! 的功能,所以,将这种实现转换为 C# 是有困难的。我们必须使用foreach 循环,遍历所有元素,这可能会导致堆栈溢出。即使能工作,由于用到大量的嵌套循环,效率也会低下。在 F# 中,针对使用 yield! 尾递归的优化,类似于常规函数调用。就是说,当序列表达式以 yield! 调用结尾,并且没有后续的代码(就像前面的这个示例),即使我们用了几个嵌套的 yield! 调用,其效率也很高。

    这个示例说明,我们可以在序列表达式中使用标准函数式模式。我们序列表达式内部使用 if 结构,以函数式风格递归地循环。F# 也允许我们使用在序列表达式内部可变状态(使用引用单元格)和命令式循环,比如 while,但是,我们很多时候不需要它们。另一方面,for 循环使用相当频繁,在本章后面讨论处理序列时会看到。

 

数组和列表中的表达式

 

    到目前为止,我们看到的序列表达式是括在大括号内,用 seq 标识符表示。这种表达式生成 seq<’a> 类型的延迟序列,它对应于标准的 .NET IEnumerable<T> 类型。F# 还提供了简单的方法,用来创建不可变的 F# 列表和 .NET 数组。下面的代码段显示了这两种集合类型:

 

> let cities =
     [ yield "Oslo"
       yield! capitals ]
;;
val cities : string list =
[ "Oslo";"London"; "Prague" ]

> let cities =
     [| yield "Barcelona"
        yield! capitals |]
;;
val cities : string array =
[| "Barcelona";"London"; "Prague" |]

 

    如你所见,我们把序列表达式的主体括在在方括号内,正如我们平常构造 F# 列表时一样,在括号后跟竖线 (|) 来构造数组。F# 把这个主体看作普通的序列表达式,并分别将结果转换为列表或数组。

    当我们使用数组或列表表达式时,整个表达式是提前计算的,因为我们需要填充所有的元素。任何副作用(如打印到控制台)将立即执行。虽然序列可以是无穷的,但是,数组和列表不行:计算直到耗尽内存为止。

 

    再看一下清单 12.4,我们生成的阶乘直到一个特定值。如果我们取消这个限制,会发生什么 (通过删除 if 条件)?在普通的 F# 中,可能会无限循环,但是,在序列表达式中会发生什么呢?答案是,我们会创建一个无穷序列,这是一个有效的、且有用的函数式结构。