コード クォート

この記事では、F# コードの式をプログラムで生成して使用できる言語機能である "コード クォート" について説明します。 この機能を使用すると、F# のコードを表す抽象構文ツリーを生成できます。 その後、アプリケーションのニーズに応じて、抽象構文ツリーを走査して処理できます。 たとえば、ツリーを使用すると、F# のコードを生成したり、他の言語でコードを生成したりできます。

引用符で囲まれた式

"引用符で囲まれた式" はコード内の F# の式であり、プログラムの一部としてコンパイルされるのではなく、F# の式を表すオブジェクトとしてコンパイルされるように区切られています。 引用符で囲まれた式は、型情報あり、または型情報なしの 2 つの方法のいずれかでマークできます。 型情報を含める場合は、記号 <@@> を使用して、引用符で囲まれた式を区切ります。 型情報が必要ない場合は、記号 <@@@@> を使用します。 次のコードでは、型が指定されている場合と指定されていない場合のクォートが示されています。

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# Quotation Evaluator を使用する必要があります。 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# コードを出力する方法を示します。 F# の式オブジェクト (Expr 型) をわかりやすい形式で表示する再帰関数 print を含む関数 println が定義されています。 FSharp.Quotations.Patterns および 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 @@>

出力

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

例 2

説明

また、ExprShape モジュールの 3 つのアクティブ パターンを使用すると、より少ないアクティブ パターンで式ツリーを走査することもできます。 これらのアクティブ パターンは、ツリーを走査したいが、ほとんどのノードではすべての情報は必要ない場合に便利です。 これらのパターンを使用すると、すべての F# の式が次の 3 つのパターンのいずれかと一致します: 式が変数である場合は ShapeVar、式がラムダ式である場合は ShapeLambda、式がそれ以外のものである場合は ShapeCombination。 前のコード例のようにアクティブ パターンを使用して式ツリーを走査する場合は、さらに多くのパターンを使用してすべての可能な F# 式の型を処理する必要があり、コードがより複雑になります。 詳しくは、ExprShape.ShapeVar|ShapeLambda|ShapeCombination アクティブ パターンに関するページをご覧ください。

より複雑な走査の基礎として、次のコード例を使用できます。 このコードでは、関数呼び出し add を含む式に対する式ツリーが作成されます。 式ツリー内の add のすべての呼び出しを検出するために、SpecificCall アクティブ パターンが使用されています。 このアクティブ パターンにより、呼び出しの引数が exprList 値に割り当てられます。 この場合は 2 つしかないため、これらが取り出され、引数に対して関数が再帰的に呼び出されます。 結果は、スプライス演算子 (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))

関連項目