8.4.2 F# 决策树
从规范中最后一句可以看出,一个链接既可以指向查询,也可以指向最终结果。在 F# 中,我们可以直接使用有两个选项的差别联合类型来写。这个规范还谈到了更多有关查询的详细信息,查询包含不同的字段,这表明它可以表示为 F# 的记录类型。
我们将定义一个 F# 的记录类型(QueryInfo),表示 有关查询的信息,和一个差别联合类型(Decision),它既可以是另一个查询,也可以是最终的结果。这些数据类型相互引用。在函数术语中,我们曾说过,类型是相互递归(mutually recursive)。清单 8.14 显示了这段 F# 源代码意味着什么。
Listing 8.14 Mutually recursive types describing decision tree (F#)
type QueryInfo =
{ Title : string
Check : Client -> bool
Positive : Decision
Negative : Decision }
and Decision =
| Result of string
| Query of QueryInfo
当在 F# 中写类型声明,我们只能引用在文件中事先声明的类型(或者,事先在编译命令中指定的文件,或者位于 Visual Studio 解决方案的更高层)。显然,如果我们要定义两个互相引用的类型,在这种情况下,会导致问题。为了解决这个问题,F# 包括 and 关键字。在清单中的类型声明开始像往常一样,用 type 关键字,但它接着用了 and,表示这两个类型同时宣布,彼此都可以看到对方。
QueryInfo 声明,在单个记录中结合了数据和行为。检查的名字是一个简单的数据成员,但其余成员更有趣。Check 成员一个函数,即,是一种行为。它可以返回一个布尔值,我们将用来在两个继续的分支中选择一个。这些分支是组合值,可能存储一个字符串,或者以递归方式包含其他 QueryInfo 值,因此,它们可以存储数据和行为。两种情况下,我们可以从这个函数返回 Decision 值,但是,我们轻易不能报告检查是否通过,我们只知道接下来的运行什么检查。在清单 8.15 中,我们创建一个值,代表 8.3 图中的决策树。
Listing 8.15 Decision tree for testing clients (F#)
let rec tree =
Query({ Title = "More than $40k"
Check = (fun cl -> cl.Income > 40000)
Positive = moreThan40; Negative = lessThan40 })
and moreThan40 =
Query({ Title = "Has criminal record"
Check = (fun cl -> cl.CriminalRecord)
Positive = Result("NO"); Negative = Result("YES") })
and lessThan40 =
Query({ Title = "Years in job"
Check = (fun cl -> cl.YearsInJob > 1)
Positive = Result("YES"); Negative = usesCredit })
and usesCredit =
Query({ Title = "Uses credit card"
Check = (fun cl -> cl.UsesCreditCard)
Positive = Result("YES"); Negative = Result("NO") })
在清单 8.15 中有一件我们之前没见过的新东西。当声明值时,我们在与新的 and 关键字的连接中使用 rec 关键字。这与先前的清单中一起声明两个 type 的用法不完全相同,但目标是类似的。and 关键字使我们能够声明相互引用的多个值(或函数)。例如,这个清单展示了在 tree 的声明中,我们如何使用值 moreThan40,尽管它在代码的后面声明。
在此示例中,声明顺序是使用 let rec 的主要原因,因为这样,我们就开始了树的根节点,然后,在第二个级上,为两个可能的选项创建值,最后,在第三级上,为一种情况声明额外的问题。我们使用 let rec,以前是为了声明递归函数,这是能够在函数体中调用自己的函数(在声明之前)。一般情况下,F# 还允许声明递归的值,这可以简化许多常见的任务。
使用递归 let 绑定初始化
我们已经看过几个递归函数的示例,但递归值会是什么样子呢?一个例子可能会是使用 Windows Forms 创建用户界面的代码。使用简化的 API,它可能看起来像这样:
let rec form = createForm "Main form" [ btn ]
and btn = createButton "Close" (fun () -> form.Close())
第一行创建窗体,并给它一个放在这个窗体上控件的列表,作为最后一个参数值。这个列表包含一个按钮,在第二行声明。CreateButton 函数的最后一个参数值是一个 lambda 函数,在用户单击该按钮时调用。它关闭应用程序,因此,需要引用窗体值,这是在第一行中声明的。
这有什么难的呢?在 C# 中,我们可以轻松地写代码来做同样的事情,而并不认为它是特别的递归。在 C# 中,我们在创建窗体之后,为按钮添加一个事件处理程序,或者,在创建窗体后添加按钮。不管哪种方法,我们都变异了对象。通过变异,两个值相互引用很容易,但是,问题在于当你想要值不可变时。
使用递归的 let 绑定,我们创建值可以引用其他值,整个序列一起声明。甚至是递归,也有其局限性,如下面的代码片断:
let rec num1 = num2 + 1
and num2 = num1 + 1
这里,我们为了获取值 num2,必须计算 num1,但是,要这样做,我们需要 num1 的值。第一个示例正确的区别在于,form 值用在 lambda 函数内部,因此,它不立即需要。幸运的是,F# 编译器可以检测到这样不能工作的代码,并生成编译错误。
我们已经向你展示了如何声明行为与数据混合的记录,以及如何使用 lambda 函数来创建记录类型的值。在清单 8.16 中,我们将完成这个示例,实现使用决策树检查客户的函数。
Listing 8.16 Recursive processing of the decision tree (F# Interactive)
> let rec testClientTree(client, tree) =
match tree with
| Result(message) �C>
printfn " OFFER A LOAN: %s" message
| Query(qinfo) �C>
let result, case =
if (qinfo.Check(client)) then "yes", qinfo.Positive
else "no", qinfo.Negative
printfn " - %s? %s" qinfo.Title result
testClientTree(client, case)
;;
val testClientTree : Client * Decision �C> unit
> testClientTree(john, tree);;
- More than $40k? no
- Years in job? no
- Uses credit card? yes
OFFER A LOAN: YES
val it : unit = ()
程序实现成一个递归函数。决策树可以是最终结果,也可以是另一个查询。在第一种情况下,打印结果。在第二种情况下,首先运行检查,并在两个可能的子树中选择一个,基于这个结果稍后处理。然后,向控制台报告进展情况,并以递归方式调用自身,以处理子树。在清单 8.16 中,我们还立即检查这个代码,看决策树哪一个路径算法符合我们示例客户。
在本节中,我们在 F# 中开发了纯函数的决策树。像我们以前见过的,在 C# 中重写函数式的结构(尤其是差别联合)可能是非常困难的,所以,在下一节中,我们将在 C# 3.0 中,通过混合面向对象技术和函数风格,来实现类似的解决方案。