程式碼引號
本文說明程式碼引號,此語言功能可讓您以程式設計方式產生及使用 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# 運算式物件新增至程式碼引號中。 您可使用 %
運算子,將具型別的運算式物件插入具型別的引號中;您可以使用 %%
運算子,將不具型別的運算式物件插入不具型別的引號中。 這兩個運算子都是一元前置運算子。 因此,如果 expr
是 Expr
型別的不具型別運算式,則下列程式碼有效。
<@@ 1 + %%expr @@>
如果 expr
是 Expr<int>
型別的具型別引號,則下列程式碼有效。
<@ 1 + %expr @>
範例 1
描述
下列範例說明使用程式碼引號,將 F# 程式碼放入運算式物件,然後列印代表運算式的 F# 程式碼。 需定義 println
函式,其中包含以好記格式顯示 F# 運算式物件 (型別為 Expr
) 的 print
遞迴函式。 FSharp.Quotations.Patterns 和 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 @@>
輸出
fun (x:System.Int32) -> x + 1
a + 1
let f = fun (x:System.Int32) -> x + 10 in f 10
範例 2
描述
您也可以使用 ExprShape 模組中的三個現用模式,周遊現用模式較少的運算式樹狀架構。 如果想要周遊樹狀架構,但不需要大部分節點中的所有資訊,這些現用模式很實用。 使用這些模式時,任何 F# 運算式都會符合下列三種模式之一:ShapeVar
(如果運算式是變數)、ShapeLambda
(如果運算式是 Lambda 運算式),或 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))