基于引用的元编程(Metaprogramming with Quotations)
在第六章,我们使用过引用(quotations),它们是被引用的 F# 代码段,引用运算符指示编译器生成表示代码的数据结构,而不是表示代码的中间语言(IL)。即,生成了表示代码的数据结构,而不是可执行代码,这样,就可以很方便地用它做你想做的事情。可以解释,执行一些动作,也可以编译成另一种语言,或者,干脆忽略它。例如,可以引用一段代码,把它编译成另一种运行时,如Java 虚拟机(JVM),或者,如第九章中的LINQ 示例一样,把它转换成 SQL,在数据库中运行。
在下面的例子中,我们将用 F# 写一个基于整数算术表达式的解析器,这对于学习如何基于堆栈进行计算是有用的。在这里,语言已经设计好了,它就是 F # 中可用的语法,所要做的就是这个算术表达式 <@ (2 * (2 - 1)) / 2 @>。就是说,无论什么时候,遇到不是整数,或不是运算符的语法时,就报错。引用是基于差别联合类型的,当使用引用时,必须通过 F# 的模式匹配和活动模式,查询接收到的表达式。例如,这里查询表达式,是通过活动模式和一个 when 子句来看它是否是整数,如果是,就压栈:
|Value (x,ty)when ty = typeof<int>->
let i = x :?>int
printfn"Push %i" i
operandsStack.Push(x :?> int)
如果不是整数,则继续检查看它是否是其他类型。也有几个参数化的活动模式会会很有用,例如,SpecificCall 接收引用作为参数,它是一个函数表达式,它可以用来查询这个引用是否匹配对这个函数的调用。可以用它来检查对一个去处符的调用是否完成,例如,这个示例检查对加法的调用是否完成:
|SpecificCall <@ (+) @> (_,_, [l;r]) ->interpretInner l
interpretInner r
preformOp (+) "Add"
清单12-5 是完整的代码。
清单12-5 基于栈的 F# 引用算术表达式的评估
open System.Collections.Generic
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns
let interpret exp =
letoperandsStack = new Stack<int>()
letpreformOp f name =
let x,y = operandsStack.Pop(), operandsStack.Pop()
printfn "%s %i, %i" name x y
letresult = f x y
operandsStack.Push(result)
let recinterpretInner exp =
matchexp with
| SpecificCall <@(*) @> (_,_, [l;r]) ->interpretInner l
interpretInner r
preformOp (*) "Mult"
| SpecificCall <@(+) @> (_,_, [l;r]) ->interpretInner l
interpretInner r
preformOp (+) "Add"
| SpecificCall <@(-) @> (_,_, [l;r]) ->interpretInner r
interpretInner l
preformOp (-) "Sub"
| SpecificCall <@(/) @> (_,_, [l;r]) ->interpretInner r
interpretInner l
preformOp(/) "Div"
| Value (x,ty) when ty =typeof<int>->
let i = x :?> int
printfn "Push: %i" i
operandsStack.Push(x :?> int)
| _ ->failwith "not a valid op"
interpretInner exp
printfn "Result: %i" (operandsStack.Pop())
interpret <@(2 * (2 - 1)) / 2 @>
运行结果如下:
Push: 2
Push: 2
Push: 1
Sub 1, 2
Multi 1, 2
Push: 2
Div 2, 2
Result: 1
[
按照原文中的程序,运行结果应为 -1。
栈是先进后出的。
对于除法与减法,顺序是不能颠倒的,即必须是除数(减数)先入栈,也就是 r 称入栈。
当然,实际上,都应该是 r 先入栈。
]
在使用引用时,我们总是用 F# 语法,它有好处,也有不足。好处是,可以产生功能强大的库,并且和 F# 代码可以很好地集成,不必要再创建解析器;不足在于,产生适合最终用户使用的工具很困难。然而,使用或转换 F# 引用的库仍然可以用在其他.NET 语言中,因为,F# 库包括了函数和示例,可以在 F# 引用和其他常用的元编程格式之间转换,比如LINQ 引用。引用的更有意义的应用,比如小语言,可参见Microsoft Solver Foundation 上的 F# DSL,http://msdn.microsoft.com/zh-cn/library/hh145003.aspx(由于变动太快,原文地址好像不能用了 http://code.msdn.microsoft.com/solverfoundation),也可以看我博客上的讨论 http://strangelights.com/blog/archive/2008/09/21/1628.aspx。
到此,我们就结束了有关 DSL 的讨论,本章的其余部分,将深入探讨有关语言解析器和编译器的实现。