下一步,我们想消除外层的模式匹配,这,使用 Option.map 是做不到的,因为这个函数总是,输入为 None,转换后输出是 None,输入为 Some,转换后输出是包含其他值的 Some。在外层的模式匹配中,我们要做的事情根本不是这样,即使输入值是 Some,而如果读第二个输入失败,仍可能返回 None。这样,作为参数值的 lambda 函数指定的类型不应该是 ‘a -> ‘b,而是 ‘a ->‘b option。
像这样的操作,用函数编程的术语,称为绑定(bind),由标准的 F# 库提供。我们看一下函数的签名,就能知道它的功能了:
> Option.bind;;
val it : ((‘a -> ‘b option) -> ‘aoption -> ‘b option) = (...)
bind 和 map 的类型签名的区别,只在于函数参数的类型,正如我们刚才讨论。只通过类型理解函数的行为,是函数程序员非常重要的技能。在这里,如果我们假定函数的行为合理,类型能够提供函数功能的很好线索,我们可以分析所有情况,推断出函数的行为规范:
■输入值为 None,bind 不能运行提供的函数,因为它不能安全地获取类型 ‘a 的值,因此,立即返回 None。
■输入值为 Some,类型 ‘a 所包含的值 x,bind 可以调用提供的函数,用 x 作为参数值。它仍然可能返回 None,但是,更合理的行为,是在可能时调用该函数。根据作为参数值的给定函数返回,有两种不同的情况:
— 如果函数返回 None,bind 操作没有类型 ‘b 的值,所以,整体结果必须返回 None。
— 如果函数返回 Some(y),那么,bind 操作有类型 ‘b 的值 y,只在这种情况下,才返回Some 结果,因此,整体结果是 Some(y)。
现在,我们可以使用绑定,重写外层的模式匹配,因为,它提供了一种即使在成功读取第一个输入时,仍然返回未定义值(None)的方法。清单 6.11 是 readAndAdd 的最终版本。
清单 6.11 使用 bind 和 map 计算两个可选值的和(F#)
let readAndAdd2() =
readInput()|> Option.bind (fun num –> [1]
readInput()|> Option.map ((+) num) ) [2]
读取第一个输入后,我们把它传递给绑定操作[1],执行给定的 lambda 函数,只在输入包含值时;在 lambda 函数内部,我们读取第二个输入,并把它映射到结果值[2];用于映射的操作把第一个输入与这个值相加。在清单中,我们把操作写成加号,和散应用,而不是显式指定 lambda 函数;如果对比清单 6.10 的代码,可以发现,这种写法肯定更简洁。现在,我们要详细分析它的工作原理了。