12.5.3 在 F# 中实现计算生成器

在计算表达式块前面的标识符,是类的实例,把所需的操作实现成为实例成员。许多操作都已经有了,我们根本不必要提供所有的,最基本的操作用 Bind 和 Return 成员实现。当 F# 编译器看到计算表达式,比如清单 12.18 时,会把它转换为使用这些成员的F# 代码。F# 示例转换成这样:

value.Bind(ReadInt(), fun n –>

value.Bind(ReadInt(), fun m–>

let add = n + m

let sub = n – m

value.Return(n * m) ))

每当我们在计算表达式中使用 let! 基本操作时,都会转换为对 Bind 成员的调用。这是因为 readInt 函数返回 ValueWrapper<int> 类型的值,但是,当我们使用自定义的值绑定,为它分配符号 n 时,值的类型将是 int;Bind 成员的目的是要从计算类型中提取值,并用这个值作为参数值,调用表示计算的其余部分函数。

可以把let! 基本操作和标准的值绑定let 的行为进行比较。在清单 12.18 中,如果写成 let n = readInt(),那么,n 的类型就是 ValueWrapper<int>,要得到整数,我们就必须自己提取。这时,我们可以使用 Value 属性,但是,对于值是隐藏的计算,唯一能访问的方式就是通过 Bind 成员。

计算的其余部分转换成函数,为计算提供了很大的灵活性。Bind 成员可能立即调用函数,或者不调用函数,就返回结果。例如,我们处理选项值时,如果Bind 成员的第一个参数是 None,那么,道最后结果就是 None,而不管这个函数的结果如何。这样,bind 操作不会调用给定的函数,因为作为参数使用的选项值是不包含实际值;在其他情况下,bind 操作应该有效地记住这个函数(通过把它作为结果的一部分保存起来),在以后执行。我们会在下一章讨论这样的例子。

我们的示例还表明,多个 let! 结构会转换成嵌套调用 Bind 成员。这是因为,作为给这个成员的最后一个参数值的函数,是连续(continuation),即,表示计算的其余部分。示例最后调用 Return 成员,是使用 return 结构创建的。

理解 bind 和 return的类型签名

各种计算表达式都需要实现的这两个操作的类型,结构总是相同;唯一的不同,是下面的签名中泛型类型 M 的变化:

Bind :     M<‘T>* (‘T -> M<‘R>) -> M<‘R>

Return : ‘T -> M<‘T>

在前面的示例中,类型 M<‘T> 是 ValueWrapper <‘T>  类型。通常,bind 操作为了调用指定的函数,需要知道如何从计算类型中获得值。当计算类型携带额外的信息时,bind 操作也需要把由第一个参数值(类型为 M <‘T>)所携带的额外信息,和从函数调用的结果(类型为 M<‘R>)提取的信息组合起来,把它们作为整体结果的一部分返回。return 操作要简单得多,因为它从基本操作值构造了一元类型的实例。

在前面的示例中,我们使用标识符 value 来构造计算,这个标识符就是普通的 F# 值,它是有特定成员对象的实例。在 F# 中,这个对象称为计算生成器(computation builder)。清单 12.19 实现了一个简单的生成器,有两个必须的成员,我们还需要创建实例 value,在转换过程中使用。

清单 12.19 为值实现计算生成器 (F#)

type ValueWrapperBuilder() =

member x.Bind(Value(v), f) =f(v)     [1]

member x.Return(v) = Value(v)    [2]

let value = new ValueWrapperBuilder()    <-- 创建生成器的实例

Bind 成员[1]首先要从ValueWrapper <‘T> 类型中提取实际值,这是在成员的参数列表中完成的,使用差别联合的 Value 识别器作为一个模式,实际值将分配给符号 v。有了这个值以后,我们就可以调用计算 f 的其余部分。这个计算类型不携带任何额外的信息,因此,我们能够返回此调用的结果,作为整个计算的结果。Return 成员就简单了,它只把值打包到计算类型中。

在这个清单中,使用 value 声明,我们现在可以运行清单 12.18 中的计算表达式。F# 还可以使用计算表达式实现 readInt 函数,我们需要把结果打包到 ValueWrapper <int> 类型的实例中,这可以使用 return 基本操作实现:

> let readInt() = value {

let n =Int32.Parse(Console.ReadLine())

return n };;

val readInt : unit ->ValueWrapper<int>

这个函数不需要 bind 操作,因为它不使用任何 ValueWrapper <‘T> 类型的值。整个函数括在计算表达式块中,这样,函数的返回类型是 ValueWrapper<int>,而不是 int。如果我们对ValueWrapper <‘T> 类型一无所知,那么,使用这个函数的唯一方法,是从另一个计算表达式中,使用 let! 基本操作来调用函数。重要的是,计算表达式提供了一种方法,通过组合,可以从简单的值生成更复杂的值。单子值有点像可以组合的黑盒子,而如果我们想看看里面,就需要一些关于单子类型的专门知识。而有了
ValueWrapper <‘T>,我们需只要知道差别联合的结构。

在 C# 中,用查询语法写出类似于 readInt 的函数,是不可能的,因为查询需要有一些输入,来初始化 from 子句。在查询语法中的 let 子句,大致相当于计算表达式中 let 绑定,但是,查询不能从这里开始。不过,如我们在清单 12.15 所见的,使用查询可以写很多有用的东西,因此,我们可以为 ValueWrapper <‘T> 类型添加查询运算符。

时间: 2024-08-27 17:41:16

12.5.3 在 F# 中实现计算生成器的相关文章

12.7.1 创建日志记录的计算

这个计算将产生一个值,并能够将消息写入到本地日志记录缓冲区.这样,计算的结果将成为一个值,和包含消息的字符串列表.同样,我们使用只有一个识别器的差别联合,表示这个类型: type Logging<'T> = | Log of 'T * list<string> 这个类型非常类似于我们先前讨论的 ValueWrapper <'a> 示例,只是加上了一个 F# 列表,表示写到日志的消息.现在,我们已有了类型,就可以实现计算生成器了.通常,我们需要实现 Bind 和 Retu

12.4.2 自定义 F# 语言

到目前为止,我们所讨论过的序列表达式,都是用 seq 标识符表示,后面的代码块括在大括号中.然而,F# 还允许我们创建自己的标识符,给代码块以特殊的意义.通常,这个功能称为计算表达式(computation expressions),而序列表达式是它的一个特例,是由 F#核心所实现,并经编译器优化. 我们已经知道,计算表达式可以包含标准的语言结构,比如 for 循环,还有别的结构,像 yield.在代码块之前的标识符,描述构造的意义,其方式与查询运算符(例如,Select 和 Where 扩展方

12.1.3 使用 F# 序列表达式 在 C# 中的迭代器非常方便(comfortable),能够在普通的 C# 方法中写复杂的代码 (实现 IEnumerable&lt;T&gt;/IEnumerator

12.1.3 使用 F# 序列表达式 在 C# 中的迭代器非常方便(comfortable),能够在普通的 C# 方法中写复杂的代码(实现 IEnumerable<T>/IEnumerator<T> 接口的类型).开发人员写的代码使用标准的C# 功能,比如环,唯一的改变只是我们可以使用一种新的语句,来做一些非标准的事情,这个新语句用 yield return 表示(或者 yield break 表示终止序列),非标准的行为返回序列中下一个元素的值.在以后需要访问序列的时候(最后,计

11.3.1.1 C# 和 F# 中的提前计算

在大多数主流的语言中,指定计算顺序的规则很简单:程序进行函数调用时,先计算所有的参数值,然后再执行函数.我们用前面的例子来演示: TestAndCalculate(Calculate(10)); 在所有的主流语言中,程序都会执行Calculate(10),然后再把结果作为参数值传递给TestAndCalculate.正如我们在前面的示例中已经看到的,如果函数 TestAndCalculate 不需要这个参数值,就很不幸了:在这种情况下,我们就无故地浪费了一些CPU 周期!这种策略称为提前计算策略

10.1 优化函数 在前面的章节中,我们已经知道,递归是 F# 中处理函数的主要控制流机制。我们第一次是使用它写一些进行计算的简单函数,例如,计算指定范围内的数字的和或阶乘。后来,我们发现它在处理递

10.1 优化函数 在前面的章节中,我们已经知道,递归是 F# 中处理函数的主要控制流机制.我们第一次是使用它写一些进行计算的简单函数,例如,计算指定范围内的数字的和或阶乘.后来,我们发现它在处理递归数据结构,最重要的列表是时,是无价的. 我们知道,递归也有一些局限性,堆栈溢出的可能性是最明显的一个:我们将会看到,某些递归计算非常低效.在命令式语言中,通常使用非递归函数,以避免出现问题:函数语言已经有方法解决这些问题,并可以高效地使用递归.首先要集中关注于正确性:如果一个额外的字节吹动堆栈,真正

9.4.2.2 F# 中的向上转换和向下转换(UPCASTS AND DOWNCASTS)

9.4.2.2 F# 中的向上转换和向下转换(UPCASTSAND DOWNCASTS) 如果类型之间的转换不会失败,就称为向上转换(upcast).我们已经看到,把类型转换成由该类型实现的接口,就是这种情况:另一个示例是把派生类转换成它的基类,在这种情况下,编译器也可以保证操作是正确的,不会失败. 如果有一个基本类型的值,希望将它转换为继承类,操作可能会失败,因为基类的值可能是目标类的值,也可能不是.在这种情况下,我们必须使用第二种类型转换,称为向下转换(downcast).让我们用一个示例来

11.2.1.2 在 F# 中写单元测试

如果我们以这种方式写直接测试的代码,很容易把它改成单元测试,成为大项目的一个部分.很快,我们将讨论如何用xUnit.net 来实现,但现在,我们要写另一个应由单元测试明确覆盖的调用:用null 值作为参数值,调用getLongest 函数: > getLongest(null);; Program.fs(24,12): error FS0043: The type 'stringlist' does not have 'null' as a proper value 这个,我们在之前还没尝试过,

10.1.2.1 C# 和 F# 中可重用的记忆化

如果看一下清单 10.3 中建立 add 值的代码,可以发现,它并不真正知道加法,只是使用了 addSimple 函数,因此,也可以处理其他任何函数.为了使代码更通用,我们可以把这个函数改成参数. 我们要写一个函数(C# 中叫方法),参数为函数,返回这个函数的记忆化版本.参数值是做实际工作的函数,返回的函数增加了缓存功能.清单 10.4 是C# 版本的代码. 清单 10.4 泛型记忆化方法 (C#) Func<T, R> Memoize<T,R>(Func<T, R>

9.4.2.1 在 F# 中实现接口

清单 9.17 使用 C# 中的显式接口实现,因为,这是 F# 允许的唯一的接口实现的风格.在函数编程风格中,这通常足够了.如果确实需要直接公开类的功能,可以添加额外的.调用相同代码的成员.清单 9.18 显示了前面示例的 F# 版本. 清单 9.18 在类中实现接口 (F#) type CoefficientTest(incomeCoeff,yearsCoeff, minValue) =  [1] let coeff(client) =