这篇文章是基于我之前为 InfoQ 撰写的名为《超越 F#基础——活动模式》的文章,介绍了另外一个新的语言特性——工作流。在 F#的介绍性书籍,《F#基础》(2007 五月,由 Apress 出版)的 11 章中介绍了纵向语言编程方式——一种使用领域特定语言(Domain Specific Language,DSL)编写程序的技术。这些 DSL 也许通过使用文本或 XML 代表具体代码的方式,来被独立实现。通常,DSL 应该通过某种数据结构和表达 DSL 的语言的其他特性,被完全地嵌入到通用编程语言的内部。工作流是建立在 F#现有的强大纵向语言编程特性的基础上的一个特性。工作流允许你捕获一小段代码来检查其中的内容,这样的特点使函数库实现者有机会把工作流用于实现 DSL 的程序模块——一种类似于 C# 3.0 中的表达式树的技术。在这篇文章中,我们将深入研究一下 F#中的工作流的工作原理。
F#是一个针对.NET 框架的静态类型化函数式编程语言。它具有 OCaml 常见的核心语言功能,以及其他流行的函数式编程语言的一些特性,并从很多其他编程语言获取了一些思想,包括 Haskell、Erlang 和 C#。简而言之,这意味着 F#是一个具有优雅语法的编程语言,当我们能交互式地执行代码的时候感觉有点像脚本编程,但是它都是类型安全且有着良好性能的编译语言。这篇文章不是 F#的介绍文章,不过网络上有很多资源可以让我们容易地学习 F#。可以参阅在我之前文章中的侧边栏所附加的一个“F#资源”列表。
创建工作流
工作流由两部分组成:用户定义的编排工作流实例的代码和定义工作流功能的函数库组件。让我们来看一个非常简单的工作流例子:
这里,我们看到一个“script”工作流绑定到一个“num”标识符上。这个工作流由两个部分组成,一部分是包含在花括弧中的 F#表达式,另外一部分是以“script”作为标识符的前缀——它可以让人们了解这个工作流的实际功能。现在让我们来看看函数基础结构是如何让这个“脚本”工作的:
这个代码稍微有点多。让我们一块一块的分开,来看看它们各自是如何与我们最初的脚本一起工作的。首先,类型定义定义了我们脚本的类型。在这里,脚本即是一个产生值的函数。我们早先定义的脚本“num”具有 Script 类型,这是由于它本身就是一个在执行时会生成一个整数值的函数(在这个例子中,这个整数值是 42)。接下来,我们定义了一个名为“runScript”的函数用于执行脚本,其也用于定义“delay”来在脚本执行之前延迟脚本的构造过程(这样,任何脚本中包含的负面影响都会在脚本被构造和执行时解决掉)。
这个函数组件的下一个主要部分是 ScriptBuilder 类。它定义了一些用于处理组成我们脚本的多个表达式的方法。这些表达式将被作为值交给这些方法处理。这里,“Return”和“Let”方法处理在这个脚本中的 let 和 return 操作。更有意思的是 let 操作。在 let 操作中“printfn”函数被用于打印出参数的值——这意味着当脚本执行时,let 操作中的值被打印到了控制台上。这种方式对于帮助调试非常有用。最后,是得到 ScriptBuilder 类的实例。在这个实例中,“Script”被用于“num”工作流中。所以当“num”脚本被执行时:
runScript num
如下内容输被出到控制台上:
2<br></br>21<br></br>val it : int = 42
开始的两个值的执行 let 操作被打印出来的值,后面的值是 F#交互机制自动打印出的这个函数的执行结果以及它的类型。
在工作流中,它不仅可以处理常规的 F#表达式(如 let 操作),正如前面所演示的,也可以新建表达式,如“let!”(发音为 let bang)和“yield”,以及其他的一些操作。这样的特性为工作流提供了真正强大的能力,允许函数库实现者创建这些关键字并赋予它们新的意义。
现在,我们将扩展一下这个例子来把“let!”表达式也包含进来。假设我们不想把每个 let 操作的结果都打印到控制台,只想让需要的操作被打印出来,则可以通过实现"let!"操作来提供一种变通的方法,允许程序员选择哪些操作被打印。下面是通过“ScriptBuilder”类中额外的方法来实现的,用于处理“let!”操作的代码:
我们也需要对“Let”做一小点修改,删除打印到控制台的代码:
那么现在当我们定义和允许脚本的时候,我们能使用 let! 操作来打印特定的 let 操作执行结果到控制台上:
现在,只有 21 会被打印到控制台:
21<br></br>val it : int = 42
F#编译器是如何处理工作流和最终执行的表达式之间的转换过程的呢?这个过程可以被称作“反语法糖(de-sugaring)”,我们将在下一节重点讲述它的工作原理。
理解 De-sugaring
工作流中的表达式可以被转化为利用“连续传递的风格(continuation passing style)”的数据结构,这一个过程就是所谓的“反语法糖”,相对而言工作流就是一种“语法糖(syntactic sugar)”。
理解“反语法糖”这个概念的最好起点还是应该回到我们那个简单例子:
被转化成:
这种优点——不必手工键入这些结构——是显而易见的。“加糖(sugared)” 的版本比既难于理解又容易出错的不加糖版本更短更容易理解和键入。比起一个原始的表达式,以这种形式完成表达式的优点是更加的灵巧。重点要注意的是“Bind”和“Let”方法被调用了。这些方法通过传入一系列参数而被调用——let 操作作为第一个参数,接着以一个等待获取值的函数作为第二个参数。这样话,就可以让我们能取巧打印出参数“p”并接着把它传递给“rest”函数继续进行计算。在“Bind”方法的定义中可以看到如下代码:
printfn <span color="#ff0000">"%A"</span> p<br></br>rest p
这是在 F#工作流非常核心的机制。连续风格的传递过程允许开发人员在一个值即将被赋值的时候插入额外的动作。在我们的例子中,这些动作稍微有点简单,但这些动作能在等待异步计算完成后被调用,然后接着执行——这就是我们将在这个系列的下一篇文章中所要讲到的异步工作流。
序列工作流
迄今为止,从我们之前简短的调试脚本已经可以看到一些这种工作流十分浅显的用法。现在让我们来深入研究利用序列工作流的一个更真实的例子。序列工作流是工作流的一种类型,它作为 F#基类库的一个部分,可以方便地被使用。序列表达式对于创建集合非常有用。例如,如下的序列表达式创建了一个具有前 3 个数的平方数和它们平方根的列表:
序列表达式的一个很有用的特性是它们能利用“yield”关键字,它被用来返回集合中的值并继续进行计算。例如,如下的代码读取一个文本文件来产生出每一行内容,由此创建一个文本文件所有行的序列:
也可以在序列表达式中使用“yield!(发音为 yield bang)”关键字。这允许你添加一个序列元素到集合里,所以它经常被用在递归序列表达式中。如下代码演示了如何使用递归序列表达式来把 WPF 控件的树形数据结构“变平”为一个序列。假如你想把某些操作应用于这个控件树形结构数据中的每项条目,这种方式就特别有用。
我们看到,在序列表达式的第一行上,我们产生了给这个集合的第一个条目。接着,在这个序列表达式的下半部,我们利用一个循环来枚举每个控件的子对象,并对这些子对象递归调用“treeToList”方法。被返回的序列就是通过“yield!”关键字来放入到最终的集合当中,如此就把一个树“变平”为一个列表了。
额外阅读
在 Don Syme、Adam Granicz 和 Antonio Cisternino 的著作《高级 F#》(Expert F#) 一书中的第 9 章涉及了工作流的更多细节。
总结
工作流是允许类库设计者创建具有巨大灵活性的函数库的一个强大技术。不仅 F#程序员可以利用工作流来创建他们自己的 DSL,而且从某种角度看,F#程序员利用工作流编写出的函数库也是很有前途的。序列工作流很有用,千万不可错过。在这个系列的下一篇文章中,将会讲述异步工作流——另外一个工作流的创新运用,其可以简化.NET 异步编程模型。
评论