函数程序表示数据,使用数据结构,我们会在第五、七章讨论数据结构。虽然数据结构的概念通常更简单,但是,我们现在要讨论复合数据类型,比如 C# 值类型,或类。从第一章我们知道,函数编程中的数据结构是不可变的。
不可变数据结构的概念,逻辑上可以从不可变值绑定的概念推导出来。典型的数据结构包含字段声明。如果我们不可变性的概念从变量声明扩展到字段声明,就能得出一切都是不可变的。在 C# 中,不可变类字段要使用 readonly 限定符,而 F# 中所有的数据结构都是不可变的,是默认情况。F# 并不是严格的函数语言,因此,它也可以创建可变类型。
此刻,我们知道如何使用不可变的数据结构,以及如何在 C# 中创建不可变类。使用这种数据结构的类的方法或函数,不能修改结构的状态,唯一可以做的就是返回值,使用这种数据结构的所有操作,结果只能返回新值。C# 中的字符串类型的行为就是如此,str.Substring(0, 5) 的结果会得到一个新的字符串,原来的字符串保持不变。
我们在第一章曾提到过,函数代码常常写成表达式,而不是语句序列。对代码的这种不同理解,使程序更具声明性,因此,使用不可变数据结构也支持函数风格。假设我们有一个类表示函数式集合,它有一个创建空列表的操作,另一个将数字“添加”到列表的操作。因为列表是不可变的,添加元素不能改变原来的列表;相反,操作会返回新的列表,其中包含原始列表的项目和新添加的元素。如果我们想要创建一个列表,并添加元素,代码可以这样写:
var res =ImmutableList.Empty<int>().Add(1).Add(3).Add(5).Add(7);
如果用可变列表,实现同样的功能,我们必须创建列表,然后通过调用命令式的 Add 方法对列表进行修改。结果,要写一个变量声明和四个语句(源代码总数可能会是五行)。这个示例表明,不可变数据结构通常有助于写出更简洁的代码。当然,命令式语言也有办法达到类似的效果,但是,用函数风格不需要额外的付出。
到目前为止,我们已经知道函数语言使用不可变的数据结构和不可变的值,而不是可变的变量。可以想象一下,不使用传统的变量和赋值运算,如何写出一些非常简单的程序。但是,一旦开始思考更复杂的问题,事情会变得困难,直到你的世界观改变为止。在下一节,我们会看到如何用函数风格实现更为复杂的计算。
2.2.2 使用不可变数据结构