# 一、基本词汇和概念

在继续如何像一个函数式程序员一样思考之前，对术语*命令式*和*函数式*有一个坚实的理解是一个好主意。但在此之前，我们将深入探讨函数式编程中最常被误解的两个术语:*巴结*和*部分应用*。扎实掌握这些术语对于理解本书后面开发的更高级的概念至关重要。系好安全带，为功能编程过山车的第一次 500 英尺下降做好准备。

## 电流与部分功能应用

如果你读过关于函数式编程的任何东西，你无疑会遇到 currying 这个术语。在 20 世纪 60 年代，克里斯托弗·斯特雷奇创造了术语“讨好”[<sup>【14】</sup>](IFP_0010.htm#_ftn14)来描述函数式编程语言实现中的一项重要技术:

在数学和计算机科学中，currying 是一种转换函数的技术，该函数有多个参数(或一组参数)，可以被称为函数链，每个函数只有一个参数(部分应用)[<sup>【15】</sup>](IFP_0010.htm#_ftn15)

相比之下，部分应用是将值绑定到参数的过程:

在计算机科学中，部分应用(或部分函数应用)是指将一个函数的多个参数固定下来，产生另一个较小的函数的过程[<sup>【16】</sup>](IFP_0010.htm#_ftn16)(“Arity”表示函数接受的参数个数。)

Currying 和 partial function application 经常被合并。[<sup>【17】</sup>](IFP_0010.htm#_ftn17)对 currying 的一些混淆与术语的使用有关，术语有时用来指程序员做的事情，有时用来指语言实现做的事情。正如 J. Storrs Hall 在 2007 年写道:“我怀疑之所以会出现这种混乱，是因为 currying 最初是一种在单参数框架中对多参数函数进行建模的技术，并且是一种元操作。在类似于 ML 的语言中，函数通常是已经存在的*，所以你看到的唯一操作是部分应用。”[<sup>【18】</sup>](IFP_0010.htm#_ftn18)在这段引语中，currying 指的是语言实现所做的事情。*

另一方面，F#架构师 Don Syme 等人说“ *Currying* 是当函数以迭代形式接受参数时使用的名称——即当函数可以部分应用时”(*专家 F# 2.0* ，第 559 页)。程序员代码中这样表示的多参数函数在语言实现中是默认的，并且可以部分应用。相比之下，多参数函数也可以表示为元组的函数，在这种情况下，元组将被语言实现视为单参数，这使得函数成为单参数函数。这意味着函数不能部分应用，因为没有其他参数。表示的选择由程序员决定，Syme 等人将程序员对语言实现中默认使用的表示的选择称为 currying，这与前面的引用相反，后者指的是当程序员做出选择时语言实现所做的事情。但是说 curried functions *可以*部分应用并不是把 currying 等同于部分应用——只是说部分应用的可能性取决于语言实现对 currying 的支持。

1.  由于部分应用依赖于 currying，这两者经常一起出现，但是它们不同，因为使用部分应用，您可以将多个参数“绑定”到一个值，并且要评估结果函数，您所需要的只是剩余的参数。我刚才陈述的三件事定义了部分应用和货币之间的区别。部分应用是将值绑定到参数的过程，这导致函数具有较少的参数。相比之下，currying 是用单参数函数的嵌套集或链来替换单个多参数函数的过程。
2.  部分应用可以通过一次绑定多个参数来执行。相比之下，currying 总是一次使用一个参数。
3.  要完全评估部分应用的函数，只需绑定尚未绑定的参数。根据部分应用的定义，必须已经绑定了一个或多个参数。相比之下，对于 curried 函数，完全求值需要绑定所有参数，并且可能已经绑定了零个或多个参数。

### 部分应用

以下示例演示了部分应用(FSI 控制台):

```fs
    > let Add5 a b c d e = a + b + c + d + e;;

    val Add5 : a:int -> b:int -> c:int -> d:int -> e:int -> int

    > Add5 1 2 3 4 5;;
    val it : int = 15
    > let Add2More = Add5 1 2 3;;

    val Add2More : (int -> int -> int)

    > Add2More 4 5;;
    val it : int = 15

```

函数`Add2More`是部分应用`Add5 1 2 3`产生的函数。`Add2More`可以通过绑定剩下的两个参数来评估，就像我在示例中所做的那样。部分应用工作，因为`Add5`是一个课程功能。这说明了部分应用对 currying 的依赖性(由 Syme 等人在前面引用的文章中指出)。顺便说一下，C#的“分部方法”与分部函数应用无关。

### 当前

Currying 由 F#语言实现处理，因此对程序员来说基本上是不可见的，但是您可以识别它将在哪里发生。当你在 F#中调用一个多参数函数而不需要做任何特别的事情时，就会发生 Currying。以下示例是多参数函数的定义，后面是对它的调用:

```fs
    > let Add5 a b c d e = a + b + c + d + e;;

    val Add5 : a:int -> b:int -> c:int -> d:int -> e:int -> int

    // the call site:

    > Add5 1 2 3 4 5;;
    val it : int = 15

```

在调用站点使用时，`Add5 1 2 3 4 5`正在调用 curried 函数`Add5 a b c d e`。F#调用不在参数周围使用圆括号或逗号。(如果函数改为由`let Add5 (a, b, c, d, e) = a + b + c + d + e`定义，它仍然会产生相同的结果，但是函数会有不同的类型。这将被语言实现理解为定义一个*单参数*函数，该函数将一个包含五个元素的元组作为输入，其中括号告诉语言实现将其中的内容作为一个可能复杂的值进行评估，逗号区分该值的独立组件。)

在前面的示例中，语言实现识别出`Add5 1 2 3 4 5`与先前定义的函数`Add5 a b c d e`的签名相匹配，因此将`Add5 1 2 3 4 5`评估为函数调用。

多参数函数由语言实现自动完成。要看到这种诱惑，我们必须躲在被窝里。

让我们用 dopeek[<sup>【19】</sup>](IFP_0010.htm#_ftn19)来反编译这个简单的 F#程序:

```fs
    open System.Security.Cryptography
    open System.Text

    [<EntryPoint>]
    let main argv =
        let Add5 a b c d e = a + b + c + d + e
        let q = Add5 1 2 3 4 5
        printfn "%i" q
        0

```

请注意函数`Add5`是如何转换为 curried 函数的(如中所示，每个函数都有一个参数，以最终函数结束，其中第二个参数是文字):

```fs
    (FSharpFunc<int, FSharpFunc<int, FSharpFunc<int, FSharpFunc<int, FSharpFunc<int, int>>>>>)

```

这就对了，`Add5 a b c d e`已经改为`FSharpFunc<’T, ‘U’>`[<sup>【20】</sup>](IFP_0010.htm#_ftn20)这样就可以用*值的形式，用*的功能。

这里需要理解的重要一点是，F#语法是这样定义的，即任何用空格指定其参数的函数都会被自动修改，仅仅通过使用这种语法，您就使用了语言实现中内置的 currying。

另一个例子:由于`Add x y`将被评估为 curried 函数(其参数用空格隔开)，我们可以直接使用分部应用或通过流水线操作，这更明确地创建分部函数应用:

```fs
    > Add 1 (Add 2 (Add 3 (Add 4 (5))));;    // Looks familiar to the decompiled F# code!
    val it : int = 15

    // - or, using pipelining –

    > 1 |> (2 |> (3 |> (4 |> (5 |> Add) |> Add) |> Add) |> Add);;
    val it : int = 15

```

### 课程

*   只要您单独表示多个参数，而不是元组或其他复杂参数，F#语言实现就会创建一个 curried 函数。
*   当您仅绑定多参数函数的前 *n* 个参数的值时，您正在创建部分函数应用。

## 什么是命令式编程？

既然我们已经揭开了函数式编程中最常被误解的术语之一的神秘面纱，让我们后退一大步，回顾一下命令式和函数式编程的前景。

“在计算机科学中，命令式编程是一种编程范式，它用改变程序状态的语句来描述计算……命令式程序定义了计算机要执行的命令序列。”[<sup>【21】</sup>](IFP_0010.htm#_ftn21)在面向对象语言上下文中，我将稍微专门化一下这个定义——“命令式编程是一种用改变*对象状态的语句来描述计算的编程范式。*”

这是命令式编程的一个基本特征。例如，这个 C#代码改变了点 *p* 的状态:

```fs
    Point p = new Point();
    p.X = 1;                 // Mutation
    p.Y = 2;                 // Mutation
    p.Offset(11, 12);        // Function with side effects.
    Console.WriteLine(p.ToString());  // ToString is for illustration purposes only.

```

这段代码由于其简单性，是一个很好的例子，说明了我们使用可变对象的频率，这些可变对象会表现出副作用。

### 什么是类？

思考类的一种方式是，它只不过是字段的容器，其中一些字段暴露于外部世界，而返回结果的方法会影响类字段的状态，或者两者都有。这就是封装的定义。当我们编写面向对象的代码时，我们编写的每个类:

*   描述维护类中字段状态所需的一切。
*   描述如何使用属性获取器和设置器等方法更改状态。
*   描述利用当前状态的计算(方法)，这些计算可能会产生导致状态改变的副作用。

这看起来像是一个过于狭窄的描述，因为它忽略了其他 OO 特性，比如继承和多态，以及类描述类型的概念，但是从根本上来说，这种描述对于每个命令类都是正确的，不管它是一个超类还是一个派生类。

上一节末尾的 C#示例说明了其中的每一点:

*   类`Point`由两个字段组成，X 和 y。
*   语句`p.X = 1`和`p.Y = 2`是属性设置函数，用于改变`p`的状态，并说明对象是可变的。
*   语句`p.ToString()`可以认为是对当前状态的计算，将 X 和 Y 转换为字符串表示。
*   语句`p.Offset(11, 12)`是改变当前状态的计算，说明了计算的副作用，因为它改变了对象的状态。

这说明了命令式编程的两个特征:可变性和副作用。

*   可变性是直接且通常显式地改变对象状态的能力。
*   在副作用中，对象被间接突变(其状态改变)，通常是隐式的。然而，副作用并不限于改变对象的状态，副作用还可以改变另一个对象、数据存储等的状态。

具有副作用的有用可变对象的一个很好的例子是文件读取器，它需要跟踪数据流中的位置，并在到达文件结尾时设置文件结尾标志。这给纯函数式编程语言带来了问题，并以有趣的方式进行处理，我们将在后面讨论。

除了前面的狭义描述之外，面向对象编程，特别是类，还提供了一些附加特性:

*   用多态性(包括默认值)统一行为模式。
*   与其他类关联，利用面向对象的继承和成员原则创建丰富的对象组合。

这些说明了命令式编程的优势，在诸如 F#这样的“不纯”函数式编程语言中，人们可以继续利用这些特性，但要小心。

### 课程

*   命令式代码非常自然地将可变性“编码”到其中。
*   命令式代码经常会有副作用。
*   额外的教训:命令式方法不被使用——编译器不支持隐式部分应用；你必须自己做。

## 什么是函数式编程？

“在计算机科学中，函数式编程是一种编程范式，一种构建计算机程序结构和元素的风格，它将计算视为数学函数的评估，并避免状态和可变数据。函数式编程强调产生结果的函数只取决于它们的输入，而不取决于程序状态——即纯数学函数。”[<sup>【22】</sup>](IFP_0010.htm#_ftn22)

在函数式编程中，我们定义的是术语，而不是命令。术语是由方程定义的，例如，这些方程可以解释如何从简单的结构构造复杂的结构。编程的工作本质上是建立方程组，以便在执行过程中通过适当的替换自动求解。

### 不变性

函数式编程的核心是强调不变性。毕竟，数学定义不会改变。没有可变对象，就没有副作用。这是通过每次出现“状态变化”时创建一个新对象来实现的——旧对象保持不变，而是创建一个新对象来反映计算结果。

在 F#中，前面 C#示例的首选实现将是不可变的。函数语言中的`=`符号表示一个等式，而不是赋值。

```fs
    type Point =
      {x: int;
      y: int;}

    let Offset p dx dy = {x = p.x + dx; y = p.y + dy}

    let p = {x=1; y=2}
    let p2 = Offset p 11 12
    ;;

```

本例中`p2`与`p`不是同一个实例，也不等于`p`，原因是函数`Offset`不变(突变)`p`；它会创建一个点的新实例。

让我们将函数`Offset`封装在一个 F#类类型中，再次保持不变性:

```fs
    type Point(x : int, y: int) =
        member self.X = x
        member self.Y = y
        member self.Offset dx dy = new Point(self.X + dx, self.Y + dy)

    let p = new Point(1, 2)
    let p2 = p.Offset 11 12

```

(从这里开始，我们将省略很多例子中的终结者`;;`。就像前面的例子一样，在最后添加它，以获得要在控制台中评估的例子。)

我们可以看到`Offset`函数显式构造了一个新的`Point`对象，从而`p <> p2`。

更新`X`和`Y`成员的唯一方法是使用以下命令显式声明它们是可变的:

*   `member val`关键字自动创建后备库。
*   `with get, set`关键字创建属性获取器和设置器。

```fs
    type Point(x : int, y: int) =
        member val X = x with get, set
        member val Y = y with get, set
        member self.Offset dx dy =
            self.X <- self.X + dx
            self.Y <- self.Y + dy
            self

    let p = new Point(1, 2)
    let p2 = p.Offset 11 12

```

在这个例子中，我们已经创建了 C# `Point`类的等价类，结果是`p2`等于`p`，实际上`p2`是 *p* 的同一个实例，因为`Offset`函数返回自身。

这不是函数式编程，所以如果你打算这样编程，就坚持使用 C#或者其他命令式编程语言。然而，它确实说明了为什么 F#被称为“不纯”的函数式编程语言。

### 一阶和高阶函数

“在计算机科学中，如果一种编程语言将函数视为一流的公民，那么它就被称为具有一流的功能。具体来说，这意味着该语言支持将函数作为参数传递给其他函数，将它们作为其他函数的值返回，并将它们分配给变量或将它们存储在数据结构中。”[<sup>【23】</sup>](IFP_0010.htm#_ftn23)

高阶函数[<sup>【24】</sup>](IFP_0010.htm#_ftn24)是可以将其他函数作为参数或者作为结果返回的函数。高阶函数是函数式编程语言的一个关键特征。“函数式编程语言的一个决定性特征是将函数提升到一流的地位。您应该能够对一个函数做您能对其他内置类型的值做的任何事情，并且能够以相当的努力程度做到这一点。”[<sup>【25】</sup>](IFP_0010.htm#_ftn25)一流函数之所以“一流”，是因为语言实现允许将它们作为一种价值来对待，所以你可以用它们做所有你可以用其他价值做的事情。

在命令式语言中，我们通常不会将函数作为参数或返回函数传递。传统上，在支持函数的地方，命令式语言实现不允许将函数视为值。就在最近，例如有了 C#的`Action`和`Func`类和 lambda 支持，就有了明确的语言能力(超越委托)来传递函数作为参数和返回函数。这些特性实际上是从函数式语言中借来的。诸如 Pascal、Fortran、BASIC 等较旧的命令式语言没有将函数作为参数传递或返回函数的功能。例如:

#### 结构中的函数

使用 C#和`Func<T, U>`类，我们可以创建一个具有相同签名的函数列表:

```fs
    static int SquareValue(int n) { return n * n; }
    static int DoubleValue(int n) { return n + n; }

    List<Func<int, int>> funcList = new List<Func<int, int>>() { SquareValue, DoubleValue };

```

(对于 C#观众，我有意避免使用类似`var funcList`的构造。)

在 F#中，我们可以更自然地实现这一点:

```fs
    let squareValue n = n * n
    let doubleValue n = n * n
    let funcList = [squareValue; doubleValue]

```

#### 作为参数的函数

我们可以将函数传递给其他函数。同样，在 C#中，这可能是这样写的:

```fs
    static int Operation(Func<int, int> f, int value) { return f(value); }

    int result1 = Operation(SquareValue, 5); // Call using defined function.
    Int result2 = Operation((n) => n * n, 6); // Call using lambda expression.

```

在 F#中，这可以写成:

```fs
    let operation f value = f value
    let result1 = operation squareValue 5
    let result2 = operation (fun n -> n * n)   6

```

注意明确使用`fun`关键字来定义 lambda 表达式。

#### 返回函数的函数

在 C#中，我们可以返回函数:

```fs
    static Func<int, int> GetMyOperation() { return SquareValue; }

    int result3 = GetMyOperation()(10);

```

在 F#中:

```fs
    let getMyOperation = squareValue
    let result3 = getMyOperation 10

```

## 这是否意味着 C#是一种函数式编程语言？

前面的例子说明了 C#是如何支持函数式编程行为的，所以答案是“某种程度上，是的。”我们将在后面看到，支持函数式编程结构的命令式语言和支持命令式编程结构的函数式语言之间是有区别的。

C#属于第一类，F#属于第二类。“纯”函数式编程语言不属于这两类。

### C #和 F#开始分叉的地方

我们可以用函数绑定和函数作为参数来做一些有趣的事情，比如编写“泛型”运算符。在 C#中，这看起来像:

```fs
    static T Adder<T>(Func<T, T, T> add, T a, T b) { return add(a, b); }

    var sum1 = Adder((a, b) => a + b, 1, 2);        // 3
    var sum2 = Adder((a, b) => a + b, 10.5, 12.6);   // 23.1
    var sum4 = Adder((a, b) => a + b, "a", "b");     // “ab”

```

这里编译器从值中推断类型，这些值必须都是相同的类型。我们可以将它用于不同的类型，因为 lambda 表达式是在需要时计算的，因此类型在该点是已知的。我们可以在 F#中使用`inline`关键字做类似的事情:

```fs
    let inline adder (a : 'T) (b : 'T) : 'T = a + b

    let sum1 = adder 1 2
    let sum2 = adder 10.5 12.6
    let sum3 = adder "a" "b"

```

这里`adder`是取两个参数`a`和`T`类型的`b`的函数，返回一个类型`T`。

有一个有趣的区别:在 C#中，我们不能创建一个分配了 lambda 表达式的泛型委托，而不在分配委托时指定类型。例如，这不起作用:

```fs
    delegate T del<T>(T a, T b);
    static del<T> adder = (a, b) => a + b;   // <<-- del<T> results in an error
    static T DlgtAdder<T>(del<T> add, T a, T b) { return add(a, b); }
    var sum5 = DlgtAdder(adder, 3, 4);

```

如果我们指定`del<int>`，那么前面的代码就起作用了。请注意，在 F#中，我们可以明确地将函数分配给一个名称，并在以后的类型推断中使用该函数。

我们还可以更进一步，提供在呼叫点执行的操作:

```fs
    let inline operation op (a : 'T) (b : 'T) : 'T = op a b
    let sum1 = operation (+) 1 2      // 3
    let diff1 = operation (-) 10 4    // 6

```

这是在命令式语言中无法做到的，即使在 C#中也是如此，除非有一些重要的变通方法。诚然，我的例子有点深奥，但是作为一个像 F#这样的函数式语言能让你做一些原本根本做不到的事情的例子，它做得很好。

### 课程

*   函数式编程避免了可变性(这有时很烦人)。
*   不可变对象不会受到副作用的影响。(因为 F#是“不纯的”，尽管如此，一个包含所有不可变字段的对象仍然可能有在其他地方引起副作用的方法，例如显示。)
*   在某种程度上，C#的语法变得过于笨拙，F#开始在语法上看起来更有吸引力。
*   在某种程度上，对函数式编程的支持在 C#中失败了，而作为函数式编程语言的 F#支持“最后一英里”