Skip to content

Latest commit

 

History

History
644 lines (408 loc) · 34.6 KB

03.md

File metadata and controls

644 lines (408 loc) · 34.6 KB

三、函数式编程

您在第 1 章中看到,纯函数式编程将一切都视为值,包括函数。虽然 F# 不是纯粹的函数式语言,但它确实鼓励你用函数式风格编程;也就是说,它鼓励您使用返回结果的表达式和计算,而不是导致某些副作用的语句。在本章中,我们将调查支持函数式编程范式的 F# 的主要语言结构,并了解它们如何使函数式编程变得更容易。

文字

文字代表常数值,是计算的有用构件。F# 有丰富的文字集,我们将在下一个示例中看到。

在 F# 中,字符串文字可以包含换行符,常规字符串文字可以包含标准转义码。逐字字符串文字使用反斜杠()作为常规字符,两个双引号("")是引号的转义码。通过使用适当的前缀和后缀指示符,可以使用十六进制和八进制定义所有整数类型。下面的示例显示了一些绑定到标识符的实际文字,这些文字在本章稍后的标识符和让绑定一节中有所描述。

    // Some strings.
    let message = "Hello
    World\r\n\t!"
    let dir = @"c:\projects"

    // A byte array.
    let bytes = "bytesbytesbytes"B

    // Some numeric types.
    let xA = 0xFFy
    let xB = 0o7777un
    let xC = 0b10010UL

功能

在 F# 中,使用关键字fun定义函数。函数的参数用空格分隔,参数用 ASCII 箭头(->)与函数体分隔。

下面是一个函数示例,该函数接受两个值并将它们相加:

    fun x y -> x + y

请注意,此函数没有名称;这是一种函数文字。以这种方式定义的函数被称为匿名函数lambda 函数,或者仅仅是 lambdas

一个函数不需要名字的想法可能看起来有点奇怪。但是,如果一个函数要作为参数传递给另一个函数,它可能不需要名称,尤其是当它执行的任务相对简单时。

如果需要给函数命名,可以将其绑定到标识符,如下一节所述。

标识符和字母绑定

标识符是您给 F# 中的值命名的方式,以便您以后可以在程序中引用它们。您可以使用关键字let定义一个标识符,后跟标识符的名称、等号和指定标识符引用的值的表达式。表达式是表示将返回值的计算的任何代码段。以下表达式显示了分配给标识符的值:

    let x = 42

对于大多数来自命令式编程背景的人来说,这看起来像是一个变量赋值。有许多相似之处,但一个关键的区别是,在纯函数编程中,一旦一个值被分配给一个标识符,它就不会改变。这就是为什么我在本书中将它们称为标识符,而不是变量

| | 注意:在某些情况下,您可以重新定义标识符。这看起来有点像标识符改变值,但它有细微的不同。此外,在 F# 的命令式编程中,标识符的值在某些情况下可能会改变。在本章中,我们将重点讨论标识符不改变其值的函数式编程。 |

标识符既可以指值,也可以指函数,既然 F# 函数本身就是值,这就不足为奇了。这意味着 F# 没有函数名或参数名的真实概念;这些只是标识符。可以像将字符串或整数文字绑定到标识符一样,将匿名函数绑定到标识符:

    let myAdd = fun x y -> x + y

但是,由于需要用名称定义函数是非常常见的,F# 为此提供了一个简短的语法。您编写函数定义的方式与编写值标识符的方式相同,只是一个函数在let关键字和等号之间有两个或多个标识符,如下所示:

    let raisePowerTwo x = x ** 2.0

第一个标识符是函数名raisePowerTwo,其后的标识符是函数参数名x。如果函数有名称,强烈建议您使用这种较短的语法来定义它。

在 F# 中声明函数的语法是无法区分的,因为函数值,F# 语法对它们的处理是相似的。例如,考虑以下代码:

    let n = 10
    let add a b = a + b

    let result = add n 4
    printfn "%i" (result)

在第一行,值10被分配给标识符n。在第二行,定义了一个add函数,该函数接受两个参数并将它们相加。请注意语法是多么相似,唯一的区别是函数有列在函数名后面的参数。因为在 F# 中一切都是值,所以第一行的文字10是值,下一行的表达式a + b的结果也是自动成为add函数结果的值。请注意,不需要像在命令式语言中那样显式地从函数返回值。

标识符名称

有一些管理标识符名称的规则。标识符必须以下划线(_)或字母开头,然后可以包含任何字母数字字符、下划线或单引号(')。关键字不能用作标识符。由于 F# 支持使用单引号作为标识符名称的一部分,因此您可以用它来表示“prime”,为不同但相似的值创建标识符名称,如下例所示:

    let x = 42
    let x' = 43

F# 支持 Unicode,因此您可以使用重音字符和非拉丁字母作为标识符名称:

    let 标识符 = 42

如果管理标识符名称的规则过于严格,可以使用双勾号(```fs`)来引用标识符名称。这允许您使用任何字符序列(只要它不包括制表符、换行符或双记号)作为标识符名称。这意味着您可以创建一个以问号结尾的标识符,例如(一些程序员认为在代表布尔值的名称后面加上问号是有用的):

    let ``more? `` = true

```fs

如果需要使用关键字作为标识符或类型名,这也很有用:

let ``class`` = "style"
例如,您可能需要使用不是用 F# 编写的库中的一个成员,并使用 F# 的一个关键字作为其名称。一般来说,最好避免过度使用这个特性,因为它可能会导致库很难从其他库中使用。NET 语言。

## 范围

标识符的*范围*定义了在程序中可以使用标识符(或类型,如下一章[定义类型](04.html#_Defining_Types)一节所述)的位置。很好地理解范围很重要,因为如果您试图使用不在范围内的标识符,您将会收到编译错误。

所有标识符—无论它们与函数还是值相关—的范围都是从它们的定义的末尾开始,直到它们出现的部分的末尾。因此,对于顶层的标识符(也就是说,不是另一个函数或其他值的本地标识符),标识符的范围是从它被定义的地方到源文件的末尾。一旦顶层的标识符被赋予一个值(或函数),这个值就不能被改变或重新定义。标识符只有在其定义结束后才可用,这意味着通常不可能根据其本身来定义标识符。

您会注意到,在 F# 中,您永远不需要显式返回值;计算的结果自动绑定到其关联的标识符。那么,如何计算函数中的中间值呢?在 F# 中,这是由空白控制的。缩进会创建一个新的作用域,这个作用域的结束由缩进的结束来表示。缩进意味着`let`绑定是计算中的中间值,在此范围之外不可见。当一个范围关闭(缩进结束)并且一个标识符不再可用时,它被称为*退出范围**退出范围*。

为了演示作用域,下面的示例显示了一个计算两个整数中间点的函数。第三和第四行显示正在计算的中间值。
// Function to calculate a midpoint.
let halfWay a b =
    let dif = b - a
    let mid = dif / 2
    mid + a

printfn "%i" (halfWay 10 20)
首先,计算两个数字之间的差值,并使用`let`关键字将其分配给标识符`dif`。为了表明这是函数中的中间值,它缩进了四个空格。空格数的选择留给程序员,但惯例是四个。之后,该示例计算中点,使用相同的缩进将其分配给标识符`mid`。最后,函数期望的结果是中点加`a`,所以代码可以简单的说`mid + a`,这就变成了函数的结果。

| ![](img/note.png) | 注意:不能使用制表符而不是空格进行缩进,因为在不同的文本编辑器中,制表符和空格可能会有所不同,当空格很大时,就会出现问题。 |

## 捕获标识符

您已经看到,在 F# 中,您可以在其他函数中定义函数。这些函数可以使用范围内的任何标识符,包括定义它们的函数的本地定义。因为这些内部函数是值,所以它们可以作为函数的结果返回,或者作为参数传递给另一个函数。这意味着,虽然标识符是在一个函数中定义的,因此其他函数看不到它,但它的实际生命周期可能比定义它的函数长得多。让我们看一个例子来说明这一点。考虑以下函数,定义为`calculatePrefixFunction`:
// Function that returns a function to
let calculatePrefixFunction prefix =
    // calculate prefix.
    let prefix' = Printf.sprintf "[%s]: " prefix
    // Define function to perform prefixing.
    let prefixFunction appendee =
        Printf.sprintf "%s%s" prefix' appendee
    // Return function.
    prefixFunction

// Create the prefix function.
let prefixer = calculatePrefixFunction "DEBUG"

// Use the prefix function.
printfn "%s" (prefixer "My message")
这个函数返回它定义的内部函数`prefixFunction`。标识符`prefix'`被定义为功能范围`calculatePrefixFunction`的本地;在`calculatePrefixFunction`外其他功能看不到。内部函数`prefixFunction`使用`prefix'`,所以当`prefixFunction`返回时,值`prefix'`必须仍然可用。`calculatePrefixFunction`创建功能`prefixer`。当调用`prefixer`时,您会看到其结果使用了一个已计算并与`prefix'`关联的值:

[调试]:我的消息

虽然你应该对这个过程有所了解,但大多数时候你不需要考虑它,因为它不涉及程序员的任何额外工作。编译器将自动生成一个*闭包*来处理将局部值的生命期扩展到定义它的函数之外。那个。NET 垃圾收集将自动处理从内存中清除该值。理解在闭包中捕获标识符的这个过程,在用命令式编程时可能更重要,在命令式编程中,标识符可以表示随时间变化的值。当以函数风格编程时,标识符将总是表示常量值,这使得计算闭包中捕获的内容稍微容易一些。

## 递归

*递归*是指根据函数本身来定义函数;换句话说,函数在其定义内调用自己。递归经常用在函数式编程中,在命令式编程中使用循环。许多人认为,用递归而不是循环来表达算法更容易理解。

要在 F# 中使用递归,请在`let`关键字后使用`rec`关键字,使标识符在函数定义中可用。下面的例子展示了递归的作用。请注意,在第五行中,函数如何对自己进行两次调用,作为其自身定义的一部分。
// A function to generate the Fibonacci numbers.
let rec fib x =
    match x with
    | 1 -> 1
    | 2 -> 1
    | x -> fib (x - 1) + fib (x - 2)

// Call the function and print the results.
printfn "(fib 2) = %i" (fib 2)
printfn "(fib 6) = %i" (fib 6)
printfn "(fib 11) = %i" (fib 11)   
该函数计算斐波那契数列中的第 *n* 项。斐波那契数列是由数列中前两个数字相加而成的,它的进展如下:11235813…递归最适合用来计算斐波那契数列,因为数列中除了前两个数字之外的任何数字的定义都取决于能够计算前两个数字,所以斐波那契数列是根据自身来定义的。

虽然递归是一个强大的工具,但是在使用它时应该小心。很容易无意中编写一个永不终止的递归函数。虽然有意编写一个不终止的程序有时是有用的,但当试图执行计算时,这很少是目标。为了确保递归函数终止,根据基本情况和递归情况来考虑递归通常是有用的:

*   *递归情况*是根据函数本身定义的值。对于函数`fib`,这是 12 以外的任何值。
*   *基本情况*是非递归情况;也就是说,在函数本身没有定义的地方,一定有一些值。在`fib`功能中,12 是基本情况。

拥有基本案例本身不足以确保终止。递归案例必须倾向于基本案例。在`fib`的例子中,如果`x`大于或等于 3,那么递归情况将倾向于基本情况,因为`x`将总是变得更小,并且在某个点达到 2。但是,如果`x`小于 1,那么`x`将持续变得更负,并且该功能将重复,直到达到机器的极限,导致堆栈溢出错误(`System.StackOverflowException`)。

前面的代码也使用了 F# 模式匹配,这将在本章后面的[模式匹配](#_Pattern_Matching)部分讨论。

## 操作员

在 F# 中,你可以把*运算符*看作是一种更具美感的调用函数的方式。

F# 有两种不同的运算符:

*   *前缀*运算符是操作数在运算符之后的运算符。
*   一个*中缀*运算符位于第一个和第二个操作数之间。

F# 提供了一组丰富多样的运算符,可以用于数字、布尔、字符串和集合类型。F# 及其库中定义的运算符太多了,本节不做介绍,所以我们不看单个运算符,而是看如何在 F# 中使用和定义运算符。

像在 C#中一样,F# 运算符是重载的,这意味着一个运算符可以使用多个类型。但是,与 C#不同,两个操作数必须是相同的类型,否则编译器会生成错误。F# 还允许用户定义和重新定义运算符。

运算符遵循一组类似于 C#的规则来解析运算符重载;因此,任何类在 BCL 或任何。用 C#编写的支持运算符重载的. NET 库将在 F# 中支持它。例如,您可以使用`+`运算符连接字符串,也可以将`System.TimeSpan`添加到`System.DateTime`,因为这些类型支持`+`运算符的重载。以下示例说明了这一点:
let rhyme = "Jack " + "and " + "Jill"
printfn "%string" rhyme

open System
let oneYearLater =
    DateTime.Now + new TimeSpan(365, 0, 0, 0, 0)
printfn "%A" oneYearLater
与函数不同,运算符不是值,因此不能作为参数传递给其他函数。但是,如果需要将运算符用作值,可以通过用括号括起来来实现。然后,该运算符的行为将完全像一个函数。实际上,这有两个后果:

*   运算符现在是一个函数,其参数将出现在运算符之后:
let result = (+) 1 1
*   因为它是一个值,所以可以作为函数的结果返回,传递给另一个函数,或者绑定到一个标识符。这提供了一种非常简洁的方式来定义`add`函数:
let add = (+)
当我们在本章后面讨论使用列表时,您将看到如何使用运算符作为值。

## 功能应用

*函数应用*,有时也称为*函数组合**组合函数*,简单来说就是用一些参数调用一个函数。以下示例显示了`add`函数被定义,然后应用于两个参数。请注意,参数没有用括号或逗号分隔;只需要空白来分隔它们。
let add x y = x + y

let result = add 4 5

printfn "(add 4 5) = %i" result
编译和执行该示例的结果如下:

(4 ^ 5)= 9

在 F# 中,函数有固定数量的参数,并应用于源文件中下一个出现的值。调用函数时不一定需要使用圆括号,但是 F# 程序员经常使用圆括号来定义哪个函数应该应用于哪个参数。考虑一个简单的情况,您想要使用`add`函数添加四个数字。您可以将每个函数调用的结果绑定到一个新的标识符,但是对于这样一个简单的计算来说,这将是非常麻烦的:
let add x y = x + y

let result1 = add 4 5
let result2 = add 6 7

let finalResult = add result1 result2
相反,将一个函数的结果直接传递给下一个函数通常更好。为此,请使用括号来显示哪些参数与哪些函数相关联:
let add x y = x + y

let result =
    add (add 4 5) (add 6 7)
这里`add`函数的第二次和第三次出现分别用参数`4`、`5`和`6`、`7`分组,第一次出现的`add`函数将作用于其他两个函数的结果。

F# 还提供了另一种使用*管道转发*操作符(`|>`)组合函数的方法。该运算符具有以下定义:
let (|>) x f = f x
这只是意味着它接受一个参数`x`,并将其应用于给定的函数`f`,因此该参数现在在函数之前给出。以下示例显示了一个参数`0.5`,该参数使用管道转发运算符应用于函数`System.Math.Cos`:
let result = 0.5 |> System.Math.Cos
这种反转在某些情况下非常有用,尤其是当您想要将许多功能链接在一起时。以下是使用管道转发运算符重写的上一个`add`函数示例:
let add x y = x + y

let result = add 6 7 |> add 4 |> add 5
一些程序员认为这种风格更易读,因为它具有使代码以从右向左的方式阅读的效果。代码现在应该是“将 67,将这个结果转发给下一个将加 4 的函数,然后将这个结果转发给将加 5 的函数。”

这个例子还利用了在 F# 中部分应用函数的能力,这将在下一节中讨论。

## 函数的部分应用

F# 支持函数的部分应用(这些有时被称为*部分**curried* 函数)。这意味着您不需要一次将所有参数传递给一个函数。请注意,上一节中的最后一个示例将一个参数传递给`add`函数,该函数接受两个参数。这与函数就是值的思想有很大关系。因此,我们可以创建一个`add`函数,向其传递一个参数,并将结果函数绑定到一个新的标识符:
let add x y = x + y

let addFour = add 4
因为一个函数只是一个值,如果它没有一次接收到所有的参数,它就会返回一个值,这个值就是一个等待其余参数的新函数。所以在这个例子中,仅仅将值`4`传递给`add`函数就产生了一个新的函数。我将函数命名为`addFour`,因为它接受一个参数,并向其添加值`4`。乍一看,这个想法可能看起来无趣且无益,但它是函数式编程的一个强大部分,你会在整本书中看到它的使用。

## 模式匹配

*模式匹配*允许您查看标识符的值,然后根据其值进行不同的计算。它可能比得上 C++和 C#中的`switch`语句,但它要强大和灵活得多。以函数风格编写的程序倾向于被编写为应用于输入数据的一系列转换。模式匹配允许您分析输入数据并决定应该对其应用哪种转换,因此模式匹配非常适合函数式编程。

F# 中的模式匹配构造允许您在各种类型和值上进行模式匹配。它也有几种不同的形式,出现在语言的几个地方。

模式匹配最简单的形式是对一个值进行匹配。你已经在本章的[递归](#_Recursion)部分看到了这一点,它被用来实现一个在斐波那契数列中生成数字的函数。为了说明语法,下一个例子显示了一个函数的实现,该函数将产生卢卡斯数,一个数字序列如下:13471118294776。卢卡斯序列的定义与斐波那契序列相同;只有起点不同。
// Definition of Lucas numbers using pattern matching.
let rec luc x =
    match x with
    | x when x <= 0 -> failwith "value must be greater than 0"
    | 1 -> 1
    | 2 -> 3
    | x -> luc (x - 1) + luc (x - 2)
模式匹配的语法使用关键字`match`,然后是要匹配的标识符,然后是关键字`with`,然后是所有可能的匹配规则,用管道(`|`)隔开。在最简单的情况下,规则由常量或标识符组成,后跟一个箭头(`->`),然后是值与规则匹配时要使用的表达式。在函数`luc`的这个定义中,第二种和第三种情况是文字-值`1`和`2`,它们将分别被值`1`和`3`代替。第四种情况将匹配任何大于 2 的`x`值,这将导致对`luc`函数的两次进一步调用。

规则按照定义的顺序进行匹配,如果模式匹配不完整,编译器会发出错误;也就是说,如果有一些可能的输入值与任何规则都不匹配。如果省略了最终规则,则`luc`函数中会出现这种情况,因为大于 2 的`x`的任何值都不匹配任何规则。如果有任何规则永远不会匹配,编译器也会发出警告,这通常是因为在它们前面还有另一个更通用的规则。如果第四条规则比第一条规则提前,那么`luc`函数就会出现这种情况。在这种情况下,其他任何规则都不会匹配,因为第一个规则将匹配`x`的任何值。

您可以添加一个`when`守卫(如示例中的第一个规则)来精确控制规则何时触发。一个`when`守卫由关键字`when`后跟一个布尔表达式组成。一旦匹配了规则,将对`when`子句进行求值,并且只有当表达式求值为`true`时,该规则才会触发。如果表达式的计算结果为`false`,剩余的规则将被搜索另一个匹配。第一个规则被设计为函数的错误处理程序。规则的第一部分是将匹配任何整数的标识符,但是`when`保护意味着规则将只匹配那些小于或等于零的整数。

如果愿意,可以省略第一个`|`。当模式匹配很小,并且您想将它放在一行上时,这可能很有用。您可以在下一个示例中看到这一点,该示例还演示了下划线(`_`)作为*通配符*的使用。
let booleanToString x =
    match x with false -> "False" | _ -> "True"
`_`将匹配任何值,并且是告诉编译器您对使用该值不感兴趣的一种方式。例如,在这个`booleanToString`函数中,你不需要在第二个规则中使用常量`true`,因为如果第一个规则匹配你就知道`x`的值将是`true`。而且不需要使用`x`来派生字符串`"True"`,可以忽略该值,直接使用`_`作为通配符即可。

模式匹配的另一个有用特性是,您可以通过使用管道(`|`)将两个模式组合成一个规则。下一个例子`stringToBoolean`演示了这一点。
// Function for converting a Boolean to a string.
let booleanToString x =
    match x with false -> "False" | _ -> "True"

// Function for converting a string to a Boolean.
let stringToBoolean x =
    match x with
    | "True" | "true" -> true
    | "False" | "false" -> false
    | _ -> failwith "unexpected input"
前两个规则有两个字符串应该评估为相同的值,所以您可以在两个模式之间使用`|`而不是有两个单独的规则。

还可以在 F# 定义的大多数类型上进行模式匹配。接下来的两个例子演示了元组上的模式匹配,两个函数使用模式匹配实现了布尔 And 和 Or。每种方法都略有不同。
let myOr b1 b2 =
    match b1, b2 with
    | true, _ -> true
    | _, true -> true
    | _ -> false

let myAnd p =
    match p with
    | true, true -> true
    | _ -> false
`myOr`函数有两个布尔参数,放在`match`和`with`关键字之间,用逗号分隔形成一个元组。`myAnd`函数有一个参数,它本身就是一个元组。无论哪种方式,为元组创建模式匹配的语法都与创建元组的语法相同和相似。

如果需要匹配元组中的值,常量或标识符用逗号分隔,标识符或常量的位置定义了它在元组中匹配的内容。这在`myOr`功能的第一和第二规则以及`myAnd`功能的第一规则中显示。这些规则将元组的一部分与常量匹配,但是如果您想在规则定义的后面处理元组的单独部分,可以使用标识符。仅仅因为您正在使用元组,并不意味着您总是需要查看组成元组的各个部分。

`myOr`的第三个规则和`myAnd`的第二个规则显示了与单个`_`通配符匹配的整个元组。如果您想使用规则定义后半部分中的值,也可以用标识符替换它。

因为模式匹配在 F# 中是如此常见的任务,所以该语言提供了替代的速记语法。如果一个函数的唯一目的是对某个东西进行模式匹配,那么使用这个语法可能是值得的。在这个版本的模式匹配语法中,您使用关键字`function`,将模式放在函数参数通常会去的地方,然后用`|`分隔所有可选规则。下一个示例在一个简单的函数中展示了这个语法,该函数递归地处理一系列字符串并将它们连接成一个字符串。
// Concatenate a list of strings into a single string.
let rec conactStringList =
    function head :: tail -> head + conactStringList tail
           | [] -> ""

// Test data.
let jabber = ["'Twas "; "brillig, "; "and "; "the "; "slithy "; "toves "; "..."]
// Call the function.
let completJabber = conactStringList jabber
// Print the result.
printfn "%s" completJabber
编译和执行该示例的结果如下:

太棒了,还有滑动的杯子...

模式匹配是 F# 的基本构造块之一,我们将在本书中多次回到它。在下一章中,我们将研究带有记录类型和联合类型的列表的模式匹配。

## 控制流程

F# 对*控制流*有很强的概念。在这方面,它不同于许多纯函数式语言,在这些语言中,控制流的概念非常松散,因为表达式可以按任何顺序进行计算。控制流的强概念在`ifthenelse…`表达式中很明显。

在 F# 中,`ifthenelse…`构造是一个表达式,意味着它返回值。根据`if`和`then`关键字之间的布尔表达式的值,将返回两个不同值中的一个。下一个例子说明了这一点。根据程序是在偶数秒还是奇数秒运行,计算`ifthenelse…`表达式以返回`"heads"`或`"tails"`。
let result =
    if System.DateTime.Now.Second % 2 = 0 then
        "heads"
    else
        "tails"

printfn "%A" result        
有趣的是`ifthenelse…`表达式只是布尔值上模式匹配的一种方便的简写。前面的例子可以改写如下:
let result =
    match System.DateTime.Now.Second % 2 = 0 with
    | true -> "heads"
    | false ->  "tails"

printfn "%A" result
`ifthenelse…`表达式有一些含义,如果你更熟悉命令式编程,你可能不会想到。F# 的类型系统要求`ifthenelse…`表达式返回的值必须是相同的类型,否则编译器会产生错误。因此,如果在前面的例子中,你用一个整数或布尔值替换了字符串`"tails"`,你会得到一个编译错误。如果您真的需要不同类型的值,您可以创建一个类型为`obj` (F# 版本的`System.Object`)的`ifthenelse…`表达式,如下例所示,它会将`"heads"`或`false`打印到控制台。
let result =
    if System.DateTime.Now.Second % 2 = 0 then
        box "heads"
    else
        box false

printfn "%A" result
命令式程序员可能会感到惊讶,如果一个表达式返回值,那么这个表达式必须有一个`else`。当你考虑刚刚看到的例子时,这是合乎逻辑的。如果将`else`从代码中移除,当`if`被评估为假时,标识符`result`不能被赋值,并且具有未初始化的标识符是 F#(以及一般的函数式编程)想要避免的。

## 列表

F# *列表*是内置于 F# 中的简单集合类型。一个 F# 列表可以是一个*空列表*,用方括号(`[]`)表示,也可以是另一个连接了一个值的列表。您可以使用内置运算符将值连接到 F# 列表的前面,该运算符由两个冒号(`::`)组成,发音为“cons”下一个示例显示了一些正在定义的列表,从第一行的空列表开始,后面是两个列表,字符串通过串联放在前面:
let emptyList = []
let oneItem = "one " :: []
let twoItem = "one " :: "two " :: []
通过串联向列表添加项目的语法有点冗长,所以如果您只想定义一个列表,可以使用速记。在这种简写表示法中,将列表项放在方括号中,并用分号(`;`)隔开,如下所示:
let shortHand = ["apples "; "pears"]
另一个处理列表的 F# 运算符是“at”符号(`@`),您可以使用它将两个列表连接在一起,如下所示:
let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]
F# 列表中的所有项目必须属于同一类型。如果您试图将不同类型的项放在列表中,例如,您试图将一个字符串连接到一个整数列表,您将会得到一个编译错误。如果需要混合类型列表,可以创建类型列表`obj`(相当于`System.Object`的 F#),如下面的代码示例所示:
// The empty list.
let emptyList = []

// List of one item.
let oneItem = "one " :: []

// List of two items.
let twoItem = "one " :: "two " :: []

// List of two items.
let shortHand = ["apples "; "pairs "]

// Concatenation of two lists.
let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]

// List of objects.
let objList = [box 1; box 2.0; box "three"]
我将在下一章[类型和类型推断](04.html#_Chapter_4_)中更详细地讨论 F# 中的类型。

F# 列表是*不可变的*。换句话说,一旦创建了列表,就不能更改。作用于列表的函数和操作符不会改变它们,但是它们会创建一个新的、修改过的列表版本,保留旧的列表供以后需要时使用。下一个例子说明了这一点。
// Create a list of one item.
let one = ["one "]
// Create a list of two items.
let two = "two " :: one
// Create a list of three items.
let three = "three " :: two

// Reverse the list of three items.
let rightWayRound = List.rev three

printfn "%A" one
printfn "%A" two
printfn "%A" three
printfn "%A" rightWayRound
创建一个包含单个字符串的 F# 列表,然后再创建两个列表,每个列表使用前一个列表作为基础。最后,将`List.rev`功能应用于最后一个列表,创建一个新的反向列表。

## 列表模式匹配

使用 F# 列表的常规方法是使用模式匹配和递归。将标题项从列表中拉出的模式匹配语法与将项连接到列表的语法相同。模式由代表头部的标识符形成,后跟`::`,然后是列表其余部分的标识符。你可以在下一个例子的`concatList`的第一条规则中看到这一点。您还可以根据列表常量进行模式匹配;你可以在`concatList`的第二条规则中看到这一点,这里有一个空列表。
// List to be concatenated.
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

// Definition of a concatenation function.
let rec concatList l =
    match l with
    | head :: tail -> head @ (concatList tail)
    | [] -> []

// Call the function.
let primes = concatList listOfList

// Print the results.
printfn "%A" primes
从列表中获取头部,对其进行处理,然后递归处理列表的尾部,这是通过模式匹配处理列表的最常见方式,但它肯定不是模式匹配和列表唯一能做的事情。以下示例显示了这种功能组合的一些其他用途。
// Function that attempts to find various sequences.
let rec findSequence l =
    match l with
    // Match a list containing exactly 3 numbers.
    | [x; y; z] ->
        printfn "Last 3 numbers in the list were %i %i %i"
            x y z
    // Match a list of 1, 2, 3 in a row.
    | 1 :: 2 :: 3 :: tail ->
        printfn "Found sequence 1, 2, 3 within the list"
        findSequence tail
    // If neither case matches and items remain,
    // recursively call the function.
    | head :: tail -> findSequence tail
    // If no items remain, terminate.
    | [] -> ()

// Some test data.
let testSequence = [1; 2; 3; 4; 5; 6; 7; 8; 9; 8; 7; 6; 5; 4; 3; 2; 1]

// Call the function.
findSequence testSequence
第一个规则演示了如何匹配一个固定长度的列表——在本例中是三个项目的列表。在这里,标识符用于获取这些项目的值,以便将它们打印到控制台。第二个规则查看列表中的前三项,看它们是否是整数 123 的序列;如果是,它会向控制台打印一条消息。最后两个规则是一个列表的标准头尾处理,旨在按照自己的方式处理列表,如果与前两个规则不匹配,就什么也不做。

编译和执行该示例的结果如下:

在列表中找到序列 123

列表中的最后 3 个数字是 3 2 1

尽管模式匹配是分析列表中数据的强大工具,但通常没有必要直接使用它。F# 库提供了许多高阶函数来处理为您实现模式匹配的列表,因此您不需要重复代码。为了说明这一点,假设您需要编写一个函数,为列表中的每个项目添加一个。您可以使用模式匹配轻松地编写它:
let rec addOneAll list =
    match list with
    | head :: rest ->
        head + 1 :: addOneAll rest
    | [] -> []

printfn "(addOneAll [1; 2; 3]) = %A" (addOneAll [1; 2; 3])
编译和执行该示例的结果如下:

(addOneAll[12;3]) = [2;3;4]

然而,对于这样一个简单的问题,代码可能比您希望的要详细一些。解决这个问题的线索来自于注意到向列表中的每个项目添加一个只是一个更一般问题的例子:需要对列表中的每个项目应用一些转换。F# 核心库包含在`List`模块中定义的`map`函数。它有以下定义:
let rec map func list =
    match list with
    | head :: rest ->
        func head :: map func rest
    | [] -> []
可以看到`map`函数的结构和前面例子中的`addOneAll`函数非常相似。如果列表不是空的,你取列表的首项,应用`func`函数,给你一个参数。然后将其附加到递归调用列表其余部分的`map`的结果中。如果列表为空,您只需返回空列表。`map`功能可用于以更简洁的方式向列表中的所有项目添加一个:
let result = List.map ((+) 1) [1; 2; 3]
printfn "List.map ((+) 1) [1; 2; 3] = %A" result

还要注意的是,这个例子使用`add`操作符作为一个函数,用括号将其括起来,如本章前面在[操作符](#_Operators)部分所述。然后,通过传递第一个参数而不是第二个参数来部分应用该函数。这创建了一个取整数并返回整数的函数,该函数被传递给`map`函数。

`List`模块包含许多其他有趣的处理列表的功能,例如`List.filter`,允许您使用谓词过滤列表,以及`List.fold`,用于创建列表摘要。

## 总结

本章简要介绍了 F# 的功能特性。这些为程序员提供了一种强大但灵活的方法来创建程序。