8.2.3 在 F# 使用闭包捕获状态

8.2.3 在 F# 使用闭包捕获状态

 

    在这一节,我们要讨论闭包(closures),在函数编程中,这是一个重要概念。闭包是很常见的,大部分时间它们都不使用可变状态。不过,出于实用的考虑,使用可变状态有时也是需要的,对于限制可变状态的范围,闭包给我们提供了一个极好的方法。

    首先,让我们来看一段简单的 F# 代码片段,我们在第五章中看过的:

 

> let createAdder num =
     (fun m -> num + m);;
val createAdder : int -> int �C> int

 

    在我们前面的讨论中,我们没有看出,在像这样的一个函数,与加法函数,它取两个参数并返回和,之间有什么区别。这是因为,我们可以调用带有一个参数值的加法函数:由于偏应用,其结果是一个函数,将指定的数加到任何给定的参数值上。

    如果你仔细分析一下在前面的例子的返回结果,它不仅是这个函数的代码!该代码是一组指令,加两个数字,但是,如果我们用两个不同的参数,调用 createAdder 两次,返回的函数显然是不同的,因为,它们是加不同的数。关键的思想是,函数不仅是代码,也是闭包,包含了这个函数使用的值,但并没有在函数体内声明。由闭包控制的值称为捕获(captured)。在前面的例子中,捕获的唯一例子是 num 参数。

    闭包听起来复杂,但很简单。 F# 编译器使用抽象类 FastFunc<int, int> 代表一个函数值,取一个整数作为参数值,并返回一个整数作为结果。清单 8.7 显示了生成的 createAdder 函数码转换为 C# 的代码。

 

Listing 8.7 Closure class generated by the F# compiler (C#)

 

class createAdder : FastFunc<int, int> {
  public int num;
  internal createAdder(int num) {
    this.num = num;
  }

  public override int Invoke(int m) {
    return this.num + m;
  }
}

static FastFunc<int, int> createAdder(int num) {
  return new createAdder(num);
}

 

    编译器生成一个静态方法 createAdder,它对应于 F# 的函数。该方法构造了一个函数值,由该函数的代码,和被这个闭包捕获到的存储的值组成。生成的封闭类取捕获到的值作为参数,因此,在我们的例子中,有一个单独的参数 num。在用虚拟方法 Invoke 执行这个函数值时,代码可以访问存储在闭包中的值。

    当然,我们一直在使用闭包创建函数,自从我们开始讨论 lambda 函数。只是没有明确地讨论,因为,通常你不需要考虑这些,它们只是工作。但是,如果闭包捕获值可能是可变的,会怎样呢?

 

可变状态使用引用单元

 

    要回答这个问题,我们需要能够创新一些状态去捕获。一个方法是用 let mutable,但是,在这种情况下,它并不工作,因为,那样的可变值只能用于本地,不能被闭包捕获。

    第二个方法是使用称为 ref 的类型创建可变值,这是创建引用单元(reference cell)的一个捷径。这是一个小的对象(实际上,是作为 F# 的记录类型声明的),它包含一个可变值。要了解 ref 类型如何工作,让我们在 C# 中定义相同的类型。正如你可以看到的,它是相当简单的:

 

class Ref<T> {
  public Ref(T value) { Value = value; }
  public T Value { get; set; }
}

 

    最重要的一点是,这里的 Value 属性是可变的,所以,当我们创建了一个不可变的 Ref<int> 类型的变量时,我们仍然可以改变它表示的值。清单 8.8 展示了一个在 F# 中使用引用单元的例子,显示了使用 Ref<T>类型相应的 C# 代码。在 F# 中,我们不直接访问这个类型,因为有一个函数,也叫 ref,创建一个引用的单元,把两个运算符连在一起,设置和读取它的值。

 

Listing 8.8 Working with reference cells in F# and C#

 

F# Interactive C#

let st = ref 10

st := 11
printfn "%d" (!st)

var st = new Ref<int>(10);

st.Value = 11;
Console.WriteLine(st.Value);


    在第一行,我们创建了一个包含整数的引用单元。就像我们刚刚在 C# 中声明的 Ref<T>  类型一样,F# 的 ref 类型是泛型的,所以,我们可以用它来存储任何类型的值。接下来的两行演示了处理引用单元的运算符:赋值(:=)和取消引用(!)。F# 的运算符对应于设置或读取属性值,但给我们一个更方便的语法。

 

在闭包中捕捉引用单元

 

    现在,我们可以编写代码来捕获,通过在闭包中,使用引用单元创建的可变状态。清单 8.9 显示了一个可配置收入检查的 F# 版本。我们创建了 createIncomeTests 函数,返回一个有两个函数的元组:第一个函数改变所需的最低收入,第二个是测试函数自身。

 

Listing 8.9 Configurable income test using closures (F# Interactive)

 

&gt; let createIncomeTest () =
     let minimalIncome = ref 30000
     (fun (newMinimal) �C&gt;
       minimalIncome := newMinimal),
     (fun (client) �C&gt;
       client.Income < (!minimalIncome))
;;
val createIncomeTest : unit -> (int -&gt; unit) * (Client -&gt; bool)

&gt; let setMinimalIncome, testIncome = createIncomeTest();;
val testIncome : (Client -&gt; bool)
val setMinimalIncome : (int -&gt; unit)

&gt; let tests = [ testIncome; (* more tests... *) ];;
val tests : (Client -&gt; bool) list

 

    首先,让我们来看一下 createIncomeTest 函数的签名,它没有取任何参数取,返回一个函数的元组作为结果。在函数体中,我们首先创建一个可变的引用单元,并把它初始化成默认最低收入。返回的这个函数元组,是通过写两个 lambda 函数完成的,两个都使用了 minimalIncome 值。第一个函数(签名 int -&gt; unit)取一个新的收入作为参数值,并修改这个引用单元。第二个比较客户的收入与存储在引用单元中的当前值,有用来检查客户的函数的通常签名(Client -&gt; bool)。

    当我们后来调用 createIncomeTest 时,结果得到两个函数。我们只创建一个引用单元,这意味着,它是由两个函数的闭包所共享。我们可以用 setMinimalIncome 改变 testIncome 函数所需的最低收入。

    让我们看一下,在这个 F# 版本和前面讨论的在 C# 中实现的命令模式之间的类似。在 F# 中,状态由闭包自动捕获,而在 C# 中,是封装在一个显式的类中。在某种意义上,函数的元组和闭包对应于面向对象模式中的接收者对象。正如我们在清单 8.7 所见到的,F# 编译器通过生成 .NET 代码处理闭包,这类似于我们在 C# 中显式写的代码。这个中间语言(IL)由 .NET 使用,并不直接支持闭包,但它有类来存储状态。

    清单 8.10 完成了这个例子,演示了如何使用 setMinimalIncome 函数,修改检查。这个例子假定 testClient 函数现在使用在清单 8.9 中声明的检查集合。为了在 F# Interactive 中完成,你需要选择,并计算声明 tests 值的绑定,然后,计算 testClient 函数,以便它引用先前计算的集合。

 

Listing 8.10 Changing minimal income during testing (F# Interactive)

 

&gt; testClient(john);;
Client: John Doe
Offer a loan: YES (issues = 1)

&gt; setMinimalIncome(45000);;
val it : unit = ()

&gt; testClient(john);;
Client: John Doe
Offer a loan: NO (issues = 2)

 

    正如在 C# 版本中一样,我们首先使用初始的 tests 来检查这个客户(传递的客户),然后,修改其中一个检查所需的收入。改变之后,
这个客户不再满足条件,结果是否定的。

 

C# 中的闭包

 

    在上一节中,我们使用 C# 来写面向对象的代码,F# 写函数式代码,因为,我们想演示这些概念是如何发生关系的,闭包如何与对象类似,特别是在命令设计模式中的接收者对象。

    对于 lambda 函数,闭包是必不可少的,在 C#3.0 中的 lambda 表达式语法也支持闭包的创建。在 C# 2 中,这是以匿名方法的形式表现的。下面的例子显示了如何创建一个函数,多次调用,将返回一个从零开始的数字序列:

 

Func<int> CreateCounter() {
  int num = 0;
  return () =&gt; { return num++; };
}

 

    变量 num 由闭包捕获,每次调用,返回的函数增加它的值。在 C# 中,变量默认是可变的,所以,当你像这样改变捕获变量的值,要特别小心。混乱的根源是捕获了一个 for 循环的循环变量。假设你在多次迭代中,捕获这个变量,到循环结束时,所有创建的闭包将包含相同的值,因为,我们只处理了一个变量。

 

    在本节中,我们讨论了面向对象模式和相关的函数技术。在某些情况下,我们使用函数,而不是有单一方法的接口。接下来,我们将看一下例子,当行为很简单,但不能只由一个函数来描述时,我们如何做。

你可能感兴趣的:(职场,休闲,闭包捕获,F#)