程式碼引號 (F#)
本主題描述「程式碼引號」(Code Quotation),這是一種可讓您以程式設計方式產生並與 F# 程式碼運算式搭配運作的語言功能。此功能可讓您產生代表 F# 程式碼的抽象語法樹狀結構。然後,可以根據應用程式的需求,來周遊和處理抽象語法樹狀結構。例如,您可以使用此樹狀結構來產生 F# 程式碼,或以某種其他語言來產生程式碼。
加引號的運算式
「加引號的運算式」(Quoted Expression) 是程式碼中的 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
@>
若要使用程式碼引號,您必須使用 open 關鍵字加入匯入宣告,而這個宣告會開啟 Microsoft.FSharp.Quotations 命名空間。
F# PowerPack 支援評估和執行 F# 運算式物件。
Expr 型別
Expr 型別的執行個體代表 F# 運算式。泛型和非泛型 Expr 型別在 F# 程式庫文件中有詳細說明。如需詳細資訊,請參閱 Microsoft.FSharp.Quotations 命名空間 (F#)和 Quotations.Expr 類別 (F#)。
接合運算子
接合可讓您合併常值程式碼引號與以程式設計方式建立的運算式或另一個程式碼引號的運算式。% 和 %% 運算子可讓您將 F# 運算式物件加入至程式碼引號。使用 % 運算子可以將具型別運算式物件插入至具型別引號,而使用 %% 運算子則可以將不具型別運算式物件插入至不具型別引號。這兩個運算子都是一元前置運算子。因此,如果 expr 是型別為 Expr 的不具型別運算式,則下列程式碼是有效的。
<@@ 1 + %%expr @@>
而且,如果 expr 是型別為 Expr<int> 的具型別引號,則下列程式碼是有效的。
<@ 1 + %expr @>
範例
描述
下列範例說明如何使用程式碼引號,將 F# 程式碼放入運算式物件,然後列印代表運算式的 F# 程式碼。定義的 println 函式包含 print 遞迴函式,可以用友善方式顯示型別為 Expr 的 F# 運算式物件。Microsoft.FSharp.Quotations.Patterns 和 Microsoft.FSharp.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 @@>
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))