코드 인용

코드 인용 (Code Quotations)

이 글에서는 코드 인용에 대해 설명할게요. 코드 인용은 F# 코드 표현식을 프로그래밍 방식으로 생성하고 다루게 해 주는 언어 기능입니다. 이 기능을 쓰면 F# 코드를 나타내는 추상 구문 트리(abstract syntax tree)를 만들 수 있어요. 그리고 이 추상 구문 트리는 여러분의 애플리케이션 요구에 따라 탐색하고 처리할 수 있습니다. 예를 들어, 이 트리를 이용해 F# 코드를 생성하거나, 다른 언어의 코드를 생성할 수도 있어요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/code-quotations

본문

인용 표현식 (Quoted expressions)

인용 표현식은 여러분의 코드 안에 있는 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> 입니다. 여기서 형식 매개변수 'T 는 F# 컴파일러의 형식 유추 알고리즘에 따라 결정된 표현식의 형식을 갖습니다. 형식 정보 없이 코드 인용을 사용하면 인용 표현식의 형식은 제네릭이 아닌 Expr 형식이 됩니다. 형식 있는 Expr 클래스의 Raw 속성을 호출하면 형식이 없는 Expr 객체를 얻을 수 있어요.

Expr 클래스에는 인용 표현식을 쓰지 않고도 F# 표현식 객체를 프로그래밍 방식으로 생성할 수 있게 해 주는 다양한 정적 메서드가 있습니다.

코드 인용은 반드시 완전한 표현식을 포함해야 합니다. 예를 들어 let 바인딩의 경우, 바인딩되는 이름의 정의와 그 바인딩을 사용하는 또 다른 표현식 둘 다가 필요합니다. 자세한 구문(verbose syntax)에서는 이 표현식이 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 type)

Expr 형식의 인스턴스는 F# 표현식을 나타냅니다. 제네릭 형식과 비제네릭 형식의 Expr 둘 다 F# 라이브러리 문서에 정리되어 있습니다. 자세한 내용은 FSharp.Quotations NamespaceQuotations.Expr Class 를 참고하세요.

접합 연산자 (Splicing operators)

접합(splicing)은 리터럴 코드 인용을, 프로그래밍 방식으로 만들었거나 다른 코드 인용에서 가져온 표현식과 결합할 수 있게 해 줍니다. %%% 연산자는 F# 표현식 객체를 코드 인용 안에 넣을 수 있게 해 줍니다. % 연산자는 형식 있는 표현식 객체를 형식 있는 인용 안에 삽입할 때 사용하고, %% 연산자는 형식 없는 표현식 객체를 형식 없는 인용 안에 삽입할 때 사용합니다. 두 연산자 모두 단항 접두 연산자입니다. 따라서 expr 이 형식 Expr 의 형식 없는 표현식이라면 다음 코드가 유효합니다.

<@@ 1 + %%expr @@>

그리고 expr 이 형식 Expr<int> 의 형식 있는 인용이라면 다음 코드가 유효합니다.

<@ 1 + %expr @>

예제 1 (Example 1)

설명 (Description)

다음 예제는 코드 인용을 사용해 F# 코드를 표현식 객체에 넣고, 그 표현식을 나타내는 F# 코드를 출력하는 방법을 보여줍니다. 재귀 함수 print 를 포함하는 println 함수가 정의되어 있는데, print 는 F# 표현식 객체(형식 Expr)를 보기 좋은 형식으로 화면에 표현합니다. FSharp.Quotations.PatternsFSharp.Quotations.DerivedPatterns 모듈에는 표현식 객체를 분석하는 데 쓸 수 있는 여러 활성 패턴(active pattern)이 있습니다. 이 예제는 F# 표현식에 등장할 수 있는 모든 패턴을 다루지는 않습니다. 인식되지 않는 어떤 패턴이든 와일드카드 패턴(_)에 매칭되어 ToString 메서드로 렌더링되는데, Expr 형식에서는 이 메서드가 매치 표현식에 추가해야 할 활성 패턴을 알려 줍니다.

코드 (Code)

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

예제 2 (Example 2)

설명 (Description)

ExprShape 모듈의 세 가지 활성 패턴을 사용하면 더 적은 활성 패턴으로 표현식 트리를 탐색할 수도 있습니다. 이 활성 패턴들은 트리를 탐색하고 싶지만 대부분의 노드에서 모든 정보가 필요하지는 않을 때 유용합니다. 이 패턴들을 사용하면, 어떤 F# 표현식이든 다음 세 가지 패턴 중 하나에 매칭됩니다. 표현식이 변수이면 ShapeVar, 람다 표현식이면 ShapeLambda, 그 외의 것이면 ShapeCombination 입니다. 앞선 코드 예제처럼 활성 패턴으로 표현식 트리를 탐색한다면, 가능한 모든 F# 표현식 형식을 다루기 위해 훨씬 더 많은 패턴을 써야 하고 코드도 더 복잡해집니다. 자세한 내용은 ExprShape.ShapeVar|ShapeLambda|ShapeCombination Active Pattern 을 참고하세요.

다음 코드 예제는 더 복잡한 탐색의 기초로 사용할 수 있습니다. 이 코드에서는 함수 호출 add 가 포함된 표현식에 대한 표현식 트리를 만듭니다. SpecificCall 활성 패턴을 사용해 표현식 트리 안의 add 호출을 탐지합니다. 이 활성 패턴은 호출의 인자들을 exprList 값에 할당합니다. 여기서는 인자가 두 개뿐이므로, 이들을 꺼내 인자들에 대해 함수를 재귀적으로 호출합니다. 그 결과는 접합 연산자(%%)를 사용해 mul 호출을 나타내는 코드 인용 안에 삽입됩니다. 앞선 예제의 println 함수를 사용해 결과를 표시합니다.

다른 활성 패턴 분기의 코드는 그저 동일한 표현식 트리를 다시 생성하므로, 결과 표현식에서 바뀌는 부분은 addmul 로 바뀐 것뿐입니다.

코드 (Code)

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

더 알아보기 (Learn more)