代码引用

本文介绍代码引用,它是一种可让你以编程方式生成和使用 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
@>

若要计算 F# 引用,必须使用 F# 引用计算器。 它为计算和执行 F# 表达式对象提供支持。

F# 引用还保留类型约束信息。 请考虑以下示例:

open FSharp.Linq.RuntimeHelpers

let eval q = LeafExpressionConverter.EvaluateQuotation q

let inline negate x = -x
// val inline negate: x: ^a ->  ^a when  ^a : (static member ( ~- ) :  ^a ->  ^a)

<@ negate 1.0 @>  |> eval

inline 函数生成的约束保留在代码引用中。 现在可以计算 negate 函数的引用形式。

Expr 类型

Expr 类型的实例表示 F# 表达式。 泛型和非泛型 Expr 类型都记录在 F# 库文档中。 有关详细信息,请参阅 FSharp.Quotations 命名空间Quotations.Expr 类

拼接运算符

拼接用于将文本代码引用与以编程方式或根据另一个代码引用创建的表达式组合在一起。 %%% 运算符用于将 F# 表达式对象添加到代码引用中。 % 运算符用于将类型化表达式对象插入类型化引用中;%% 运算符用于将非类型化表达式对象插入非类型化引用中。 这两个运算符都是一元前缀运算符。 因此,如果 exprExpr 类型的非类型化表达式,则以下代码有效。

<@@ 1 + %%expr @@>

如果 exprExpr<int> 类型的类型化引用,则以下代码有效。

<@ 1 + %expr @>

示例 1

描述

以下示例演示如何使用代码引用将 F# 代码放入表达式对象,然后打印表示该表达式的 F# 代码。 该示例定义了函数 println,后者包含以友好格式显示 F# 表达式对象(Expr 类型)的递归函数 printFSharp.Quotations.PatternsFSharp.Quotations.DerivedPatterns 模块中有几个活动模式可用于分析表达式对象。 此示例并未包含可能出现在 F# 表达式中的所有可能模式。 任何无法识别的模式都会触发与通配符模式 (_) 的匹配,并使用 ToString 方法呈现,该方法(在 Expr 类型上)可让你知道要添加到匹配表达式的活动模式。

代码

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 @@>

输出

fun (x:System.Int32) -> x + 1
a + 1
let f = fun (x:System.Int32) -> x + 10 in f 10

示例 2

描述

你还可以使用 ExprShape 模块中的三个活动模式来遍历活动模式较少的表达式树。 如果希望遍历树,但并不需要大多数节点中的所有信息,这些活动模式会很有用。 当你使用这些模式时,任何 F# 表达式都会匹配以下三种模式之一:如果表达式是变量,则为 ShapeVar;如果表达式是 Lambda 表达式,则为 ShapeLambda;如果表达式是其他任何形式,则为 ShapeCombination。 如果像前面的代码示例那样使用活动模式来遍历表达式树,则必须使用更多模式来处理所有可能的 F# 表达式类型,代码也会更复杂。 有关详细信息,请参阅 ExprShape.ShapeVar|ShapeLambda|ShapeCombination 活动模式

以下代码示例可用作更复杂遍历的基础。 此代码为涉及函数调用 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

输出

1 + Module1.add(2,Module1.add(3,4))
1 + Module1.mul(2,Module1.mul(3,4))

另请参阅