代码引用 (F#)
本主题介绍代码引用,这是一种可让您以编程方式生成和处理 F# 代码表达式的语言功能。 该功能使您生成一个表示 F# 代码的抽象语法树。 然后可以根据应用程序的需要来遍历和处理该抽象语法树。 例如,可以使用该树生成 F# 代码或生成其他语言的代码。
引用的表达式
“引用的表达式”是代码中的一种 F# 表达式,这类表达式在代码中被分隔,从而不会作为程序的一部分进行编译,而是编译为表示 F# 表达式的对象。 可以按以下两种方式之一标记引用的表达式:包含类型信息或不包含类型信息。 如果要包含类型信息,请使用符号 <@ 和 @> 来分隔引用的表达式。 如果不需要类型信息,请使用符号 <@@ 和 @@>。 下面的代码显示了类型化的引用和非类型化的引用。
open Microsoft.FSharp.Quotations
// A typed code quotation.
let expr : Expr<int> = <@ 1 + 1 @>
// An untyped code quotation.
let expr2 : Expr = <@@ 1 + 1 @@>
在遍历一个大型表达式树,如果不包含类型信息,则遍历速度将更快。 用类型化符号引起来的表达式的生成类型为 Expr<'T>,其中的类型参数具有表达式的类型(由 F# 编译器的类型推理算法确定)。 当使用不含类型信息的代码引用时,引用的表达式的类型为非泛型类型 Expr。 可以调用类型化的 Expr 类的 Raw 属性以获取非类型化的 Expr 对象。
有各种静态方法可让您在 Expr 类中以编程方式生成 F# 表达式对象,而不必使用引用的表达式。
请注意,代码引用必须包括一个完整的表达式。 例如,对于 let 绑定,需要绑定名称的定义和使用此绑定的一个附加表达式。 在详细语法中,这是一个跟在 in 关键字之后的表达式。 在模块的顶级,这恰好是模块的下一个表达式,但在引用中,它是显式需要的。
因此,下面的表达式是无效的。
// Not valid:
// <@ let f x = x + 1 @>
但下面的表达式是有效的。
// Valid:
<@ let f x = x + 10 in f 20 @>
// Valid:
<@
let f x = x + 10
f 20
@>
若要使用代码引用,必须添加一个可以打开 Microsoft.FSharp.Quotations 命名空间的导入声明(通过使用 open 关键字)。
F# PowerPack 为计算和执行 F# 表达式对象提供支持。
Expr 类型
一个 Expr 类型的实例表示一个 F# 表达式。 F# 库文档中对泛型和非泛型的 Expr 类型都进行了描述。 有关更多信息,请参见 Microsoft.FSharp.Quotations 命名空间 (F#) 和 Quotations.Expr 类 (F#)。
接合运算符
可以使用接合操作将文本代码引用与以编程方式创建的表达式或来自另一个代码引用的表达式合并。 您可以使用 % 和 %% 运算符将 F# 表达式对象添加到代码引用中。 使用 % 运算符将类型化的表达式对象插入到类型化的引用中;使用 %% 运算符将非类型化的表达式对象插入到非类型化的引用中。 两种运算符都是一元前缀运算符。 因此,如果 expr 为 Expr 类型的非类型化的表达式,则下面的代码是有效的。
<@@ 1 + %%expr @@>
并且,如果 expr 为 Expr<int> 类型的类型化的 引用,则下面的代码是有效的。
<@ 1 + %expr @>
示例
说明
下面的示例阐释如何使用代码引用将 F# 代码放在一个表达式对象中,然后打印表示该表达式的 F# 代码。 函数 println 被定义为包含递归函数 print,后者以友好格式显示 F# 表达式对象(属于 Expr 类型)。 在 Microsoft.FSharp.Quotations.Patterns 和 Microsoft.FSharp.Quotations.DerivedPatterns 模块中有若干可用于分析表达式对象的活动模式。 此示例不包括可能会在 F# 表达式中出现的所有可能的模式。 任何无法识别的模式都会触发与通配符模式 (_) 的匹配,并使用 Expr 类型的 ToString 方法来呈现,您可以通过该方法了解将添加到匹配表达式的活动模式。
代码
module Print
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns
let println expr =
let rec print expr =
match expr with
| Application(expr1, expr2) ->
// Function application.
print expr1
printf " "
print expr2
| SpecificCall <@@ (+) @@> (_, _, exprList) ->
// Matches a call to (+). Must appear before Call pattern.
print exprList.Head
printf " + "
print exprList.Tail.Head
| Call(exprOpt, methodInfo, exprList) ->
// Method or module function call.
match exprOpt with
| Some expr -> print expr
| None -> printf "%s" methodInfo.DeclaringType.Name
printf ".%s(" methodInfo.Name
if (exprList.IsEmpty) then printf ")" else
print exprList.Head
for expr in exprList.Tail do
printf ","
print expr
printf ")"
| Int32(n) ->
printf "%d" n
| Lambda(param, body) ->
// Lambda expression.
printf "fun (%s:%s) -> " param.Name (param.Type.ToString())
print body
| Let(var, expr1, expr2) ->
// Let binding.
if (var.IsMutable) then
printf "let mutable %s = " var.Name
else
printf "let %s = " var.Name
print expr1
printf " in "
print expr2
| PropertyGet(_, propOrValInfo, _) ->
printf "%s" propOrValInfo.Name
| String(str) ->
printf "%s" str
| Value(value, typ) ->
printf "%s" (value.ToString())
| Var(var) ->
printf "%s" var.Name
| _ -> printf "%s" (expr.ToString())
print expr
printfn ""
let a = 2
// exprLambda has type "(int -> int)".
let exprLambda = <@ fun x -> x + 1 @>
// exprCall has type unit.
let exprCall = <@ a + 1 @>
println exprLambda
println exprCall
println <@@ let f x = x + 10 in f 10 @@>
Output
fun (x:System.Int32) -> x + 1
a + 1
let f = fun (x:System.Int32) -> x + 10 in f 10
示例
说明
还可使用 ExprShape 模块中的三个活动模式来遍历包含较少的活动模式的表达式树。 当您想要遍历某个树但不需要其中大部分节点中的所有信息时,这些活动模式会很有用。 当使用这些模式时,任何 F# 表达式将会与以下三个模式之一匹配:ShapeVar(如果表达式是一个变量)、ShapeLambda(如果表达式是一个 lambda 表达式)或 ShapeCombination(如果表达式是任何其他内容)。 如果您通过使用前面的代码示例中的活动模式遍历表达式树,则必须使用更多的模式来处理所有可能的 F# 表达式类型,并且代码将更为复杂。 有关更多信息,请参见 ExprShape.ShapeVar|ShapeLambda|ShapeCombination 活动模式 (F#)。
下面的代码示例可用作更为复杂的遍历的基础。 在此代码中,将为一个涉及函数调用 (add) 的表达式创建一个表达式树。 SpecificCall 活动模式用于检测表达式树中对 add 的任何调用。 此活动模式将调用参数分配给 exprList 值。 在此例中,只有两次调用,因此将提取出这两次调用,并以递归方式对参数调用函数。 通过使用接合运算符 (%%),将结果插入到表示对 mul 的调用的代码引用中。 前面的示例中的 println 函数用于显示结果。
其他活动模式分支中的代码只是重新生成相同的表达式树,因此结果表达式中的唯一更改是将 add 更改为 mul。
代码
module Module1
open Print
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.DerivedPatterns
open Microsoft.FSharp.Quotations.ExprShape
let add x y = x + y
let mul x y = x * y
let rec substituteExpr expression =
match expression with
| SpecificCall <@@ add @@> (_, _, exprList) ->
let lhs = substituteExpr exprList.Head
let rhs = substituteExpr exprList.Tail.Head
<@@ mul %%lhs %%rhs @@>
| ShapeVar var -> Expr.Var var
| ShapeLambda (var, expr) -> Expr.Lambda (var, substituteExpr expr)
| ShapeCombination(shapeComboObject, exprList) ->
RebuildShapeCombination(shapeComboObject, List.map substituteExpr exprList)
let expr1 = <@@ 1 + (add 2 (add 3 4)) @@>
println expr1
let expr2 = substituteExpr expr1
println expr2
Output
1 + Module1.add(2,Module1.add(3,4))
1 + Module1.mul(2,Module1.mul(3,4))