티스토리 툴바


Application

F# quotation expression으로 무엇을 할 수 있을까? 예를 들면, F# Code를 특정 타입으로 바꾸어 보자.


우선 다음과 같은 DSL이 있다고 하자.

 // e := n | e + e | e * e
type exp = Num of int | Add of exp * exp | Mul of exp * exp


이 DSL의 실행기는 다음과 같다.

let rec eval exp =
    match exp with
    | Num n -> n
    | Add (e1, e2) -> eval e1 + eval e2
    | Mul (e1, e2) -> eval e1 * eval e2 


예를 들면 다음과 같다:

 > eval (Add (Mul (Num 5, Add (Num 4, Num 2)), Num(2)))
val it : int = 32


이제 F# code를 DSL로 변환하는 것을 생각해 보자. 예상대로 Quotation을 이용한다:

> let rec trans code =
     match code with
     | Int32(n) -> Num n
     | SpecificCall <@ (+) @> (_, _, [lhs; rhs]) -> 
       Add (trans lhs, trans rhs)
     | SpecificCall <@ (*) @> (_, _, [lhs; rhs]) ->
       Mul (trans lhs, trans rhs)
     | expr -> failwith "Unknown Expr:\n%A" expr 
val trans : Expr -> exp


사용예를 보자:


> trans <@ 1 + 2 @>
val it : exp = Add (Num 1,Num 2)

> trans <@ 2 * 3 + 4 @>
val it : exp = Add (Mul (Num 2,Num 3),Num 4)

> trans <@ (2 + 10) * ( 3 * ( 2 + 6 * 7 + 2 )) @> 
val it : exp =
  Mul
    (Add (Num 2,Num 10),Mul (Num 3,Add (Add (Num 2,Mul (Num 6,Num 7)),Num 2)))


trans 함수는 F# code를 받아서 이를 DSL로 변경시켜준다. 따라서, 실행기에 넘길 수 있다:

> eval <| trans <@ 1 + 2 @>
val it : int = 3 

> eval <| trans <@ 2 * 3 + 4 @>
val it : int = 3 

> eval |< trans <@ (2 + 10) * ( 3 * ( 2 + 6 * 7 + 2 )) @>
val it : int = 3 


이제 다시 큰그림을 그려 보자. 우리에게 특정 언어와 그 언어 실행기가 있다면, quotation은 F# code를 그 실행기에서 실행할 수 있는 방법을 제공해 주고 있는 것이다. 즉, .NET 이외의 다른 플랫폼에서 실행할 수 있는 방법을 제공해 준다는 것인데, F# code로 작성된 프로그램을 GPU에서 돌리거나, 자동으로 SQL을 생성한다는지, JavaScript으로 변환하는 프로젝트들이 이런 방법을 쓴 예가 되겠다.


Generating Quotation Expressions

이번에는 quotation expression을 생성하는 것에 대해서 살펴 보자. 기본적으로 active pattern을 이용해서 quotation을 분해할 수 있었는데, 그 반대로 quotation을 생성하는 static method들이 있다. 예를 들어서, 다음을 보자:

> let a = <@ let x = (1, 2, 3) in (x, x) @> 
val a : Expr<(int * int * int) * (int * int * int)> =
  Let (x, NewTuple (Value (1), Value (2), Value (3)), NewTuple (x, x))


동일한 Expr<_> 값을 다음처럼 생성할 수 있다:


> let b =
  Expr.Let(
    new Var("x", typeof<int * int * int>),
    Expr.NewTuple( [Expr.Value(1); Expr.Value(2); Expr.Value(3)] ),
    Expr.NewTuple( [Expr.GlobalVar("x").Raw; Expr.GlobalVar("x").Raw] ))
val b : Expr =
  Let (x, NewTuple (Value (1), Value (2), Value (3)), NewTuple (x, x))

 

Expression Hole

Quotation은 expression hole이라고 불리는 것을 포함할 수 있는데, 이는 Expr<_> 값이 들어갈 placeholder같은 개념이다. 간단한 예를 보자. 

> let addTwoQuotations x y = <@ %x + %y @>
val addTwoQuotations :  Quotations.Expr<int> -> Quotations.Expr<int> -> Quotations.Expr<int>

> addTwoQuotations <@ 1 @> <@ 2 @>
val it : Quotations.Expr<int> =
  Call (None, op_Addition, [Value (1), Value (2)])
    {CustomAttributes = [NewTuple (Value ("DebugRange"),
          NewTuple (..))]; Raw = ...; Type = System.Int32;}


위에서 x, y 변수는 Expr<int> 타입을 갖는다. 좀더 복잡한 값을 넘길 수도 있다:

> addTwoQuotations <@ [1; 2; 3] |> List.fold (+) 0 @>
                   <@
"hello".Length @> 


즉, 타입이 int를 갖는 F# code block을 인자로 받아서 미리 준비된 quotation expression과 합성을 할 때 쓰인다.


Evaluation Quotation

그럼, 이렇게 작성된 quotation expression을 실행하거나 값을 구하는 방법은 없을까? F# PowerPack을 사용하면 F# quotation expression의 값을 구하거나 실행할 수 있다. 위의 예제를 값을 구해보자.


(* in fsi,
#r "FSharp.PowerPack.dll"
#r "FSharp.PowerPack.Linq.dll"
*)

open Microsoft.FSharp.Linq.QuotationEvaluation

let addTwoQuotations x y = <@ %x + %y @>
let x = addTwoQuotations <@ 1 @> <@ 2 @>
let y = addTwoQuotations <@ [1; 2; 3] |> List.fold (+) 0 @> <@ "hello".Length @>

> x.Eval()
val it : int = 3 

> y.Eval()
val it : int = 11 


이때, Eval 함수는 unit -> int의 타입을 갖는다. Eval 이외에도 Compile 함수가 제공된다:


 > let z = x.Compile()
val z : (unit -> int) 

> z ()
val it : int = 3


Compile 함수는 unit -> unit -> int의 타입을 갖는다.


마치며

Quotation은 컴파일러에게 "이 부분은 코드를 생성하지 말고, 대신에 expression tree를 만들라"는 것인 셈이다. 구해진 expression tree로 앞에서 살펴본대로 다양하게 목적에 맞게 사용할 수 있다. 한가지 문제는 Quotation expression은 F# 컴파일러가 알아 볼 수 있는 형태, 즉, F# code여야 한다는 점이다. 하나의 소스(F# code)로 다양한 플랫폼에서 실행가능하게 하고 싶은 경우에 적합하지만, 전혀 다른 문법의 언어를 사용하고 싶다면 lexer나 parser가 필요하겠다. 






Posted by PLTeacher

Quoting Method Bodies (From Programming F#)

다음 예를 보자. 전의 코드에 Lambda 경우가 추가되었다:

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns

let rec describeCode (expr : Expr) =
    match expr with
    // literal values
    | Int32(i) -> printfn "Integer with value %d" i
    | Double(f) -> printfn "Floating-point with value %f" f
    | String(s) -> printfn "String with value %s" s

    // function calls
    | Call(calledOnObject, methInfo, args) ->
        let calledOn = match calledOnObject with
                       | Some(x) -> sprintf "%A" x
                       | None -> "(Called a static method)"

        printfn "Calling method '%s': \n\
                 On value: %s \n\
                 With args: %A" methInfo.Name calledOn args

    // Lambda expression
    | Lambda(var, lambdaBody) ->
        printfn "Lambda Expression on value '%s' with type '%s'" var.Name var.Type.Name
        printfn "Processing body of Lambda Expression..."
        describeCode lambdaBody

    | _ -> printfn "Unknown expression form:\n%A" expr


사용예를 보자. 먼저, Lambda 경우:

> <@ fun x -> x + 1 @>
val it : Expr<(int -> int)> =
  Lambda (x, Call (None, op_Addition, [x, Value (1)]))
    {CustomAttributes = [NewTuple (Value ("DebugRange"),         NewTuple (Value ("..."), Value (40), Value (3), Value (40), Value (17)))]; Raw = ...; Type = Microsoft.FSharp.Core.FSharpFunc`2[System.Int32,System.Int32];}

describeCode <@ fun x -> x + 1 @>
Lambda Expression on value 'x' with type 'Int32'
Processing body of Lambda Expression...
Calling method 'op_Addition':
On value: (Called a static method)
With args: [x; Value (1)] 


패턴 매칭에서 두 변수에 값이 매핑되어 var는 'x'를, lambdaBody는 'x + 1'이 되거, lambdaBody는 다시 Expr이기때문에 describeCode를 통해서 Call 처리가 된다.

static member Lambda : Var * Expr -> Expr

또 다른 예제를 보자:

let add x = x + 1
val add : int -> int

<@ add 5 @>
val it : Expr<int> = Call (None, add, [Value (5)])
    {CustomAttributes = [NewTuple (Value ("DebugRange"),
          NewTuple (Value ("..."), Value (43), Value (3), Value (43), Value (8)))]; Raw = ...; Type = System.Int32;}

describeCode <@ add 5 @>
Calling method 'add':
On value: (Called a static method)
With args: [Value (5)]

위의 경우를 보면 add의 경우 더 이상 메소드 내부로 들어가지 않음을 볼 수 있다. 이게 F#의 기본 설정인데 메소드 내부도 Quotation에 포함되게 하려면 해당 메소드에 [<ReflectedDefinition>] 속성이 설정되어 있어야 하고, 이를 매칭할 때는 MethodWithReflectedDefinition 패턴을 사용해야 한다.

즉, 먼저 descibeCode에서 Call 경우가 다음처럼 변경되어야 한다:


     | Call(calledOnObject, methInfo, args) ->
        let calledOn = match calledOnObject with
                       | Some(x) -> sprintf "%A" x
                       | None -> "(Called a static method)"

        printfn "Calling method '%s': \n\
                 On value: %s \n\
                 With args: %A" methInfo.Name calledOn args

        match methInfo with
        | MethodWithReflectedDefinition(methBody) ->
            printfn "Expanding method body of '%s'..."
                    methInfo.Name
            describeCode methBody
        | _ -> printfn "Unable to expand body of '%s''. Quotation stops here." methInfo.Name


위의 예를 다시 실행시켜 보자:

[<ReflectedDefinition>]
  let add x = x + 1
val add : int -> int

> <@ add 5 @>
val it : Expr<int> = Call (None, add, [Value (5)])
    {CustomAttributes = [NewTuple (Value ("DebugRange"),
          NewTuple (Value ("..."),  Value (45), Value (3), Value (45), Value (8)))]; Raw = ...; Type = System.Int32;}

> describeCode <@ add 5 @>
Calling method 'add':
On value: (Called a static method)
With args: [Value (5)]
Expanding method body of 'add'...
Lambda Expression on value 'x' with type 'Int32'
Processing body of Lambda Expression...
Calling method 'op_Addition':
On value: (Called a static method)
With args: [x; Value (1)]
Unable to expand body of 'op_Addition''. Quotation stops here.

이번 경우에는 add의 메소드 바디 (즉, x + 1) 까지 들여다 볼 수 있다.


Decomposing Arbitrary Code

Quoted expression 즉, AST에 대해서 패턴매칭을 하는 경우 모든 경우를 다 나열하는 것이 비효율적이기때문에 "catchall"같은 것이 필요한데, F#에서 ShapeVar, ShapeLambda, ShapeCombination active pattern을 소개하고 있다.

간단한 예제를 살펴보자:

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.ExprShape

let
rec describeCode2 indentation expr =
    let indentedMore = indentation + "  "
    match expr with
    | ShapeVar(var) ->
        printfn "%s Looking up value '%s'" indentation var.Name
    | ShapeLambda(var, lambdaBody) ->
        printfn "%s Lambda expression, introducing var '%s'"
                indentation var.Name
        describeCode2 indentedMore lambdaBody
    | ShapeCombination(_, exprs) ->
        printfn "%s ShapeCombination:" indentation
        exprs |> List.iter (describeCode2 indentedMore) 

사용예를 보자:

> describeCode2 "" <@ (fun x y z -> x + y + z) @>

Lambda expression, introducing var 'x'
   Lambda expression, introducing var 'y'
     Lambda expression, introducing var 'z'
       ShapeCombination:
         ShapeCombination:
           Looking up value 'x'
           Looking up value 'y'
         Looking up value 'z'

이 틀에서 원하는 패턴을 적당한 위치에 넣는 식으로 작업을 하게 된다.





Posted by PLTeacher

Code quotation을 이용하면 주어진 F# code로부터 이에 해당하는 AST값을 얻을 수 있다. 이 값으로 다음과 같은 일을 할 수 있다 (From Programming F#):

- 코드 분석 및 검사 (inspection)

- 다른 플랫폼(SQL, GPU)에서 실행

- 새로운 코드 생성

이게 다 무슨 의미일까 싶은데...

Quotation Basics

간단한 예로 시작: 

> <@ 1 + 1 @>;;
val it : Quotations.Expr<int> =
Call (None, op_Addition, [Value (1), Value (1)])
{CustomAttributes = [NewTuple (Value ("DebugRange"),NewTuple (Value ("...\Program.fs"),
Value (3), Value (3), Value (3), Value (8)))];
Raw = ...;Type = System.Int32;}

> <@@ 1 + 1 @@>;;
val it : Quotations.Expr =Call (None, op_Addition, [Value (1), Value (1)]){CustomAttributes = [NewTuple (Value ("DebugRange"),NewTuple (Value ("stdin"), Value (16), Value (4), Value (16),Value (9)))];Type = System.Int32;}

이 예에서 <@ 1 + 1 @>는 타입이 Expr<int>가 되는데, 1 + 1의 타입이 int이기 때문이다. <@@ @@>는 타입 정보를 갖지 않는 차이가 있다. <@ 1 + 1 @>.Raw는 <@@ 1 + 1 @@>과 동일하다.

object expression을 제외한 모든 F# 코드가 quoted expression이 될 수 있다. Quoted expression은 일반 코드처럼 컴파일되는 것이 아니라 F# 코드를 나타내는 객체(AST)로 바뀐다.

물론 quoted expression을 사용하지 않고 직접 Expr 클래스를 이용해서도 동일한 효과를 얻을 수 있다.  

> let x = <@ 1 @>;;
val x : Quotations.Expr<int> = Value (1)

> let y = Quotations.Expr.Value(1);;
val y : Quotations.Expr = Value (1)
 

 

Decomposing Quotations

일단 quotation을 갖게 되면, active pattern을 이용해서 AST를 분해할 수 있다. 이때 <@@ @@>의 경우는 타입 정보를 갖지 않기때문에 좀더 빠를 수 있다. 미리 제공되는 active pattern을 사용하기 위해서는 다음 네임스페이스를 추가해야 한다: 

open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns

AST를 분해하는 간단한 예를 보자: 

open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
open Microsoft.FSharp.Quotations.DerivedPatterns

let rec describeCode (expr : Expr) =
    match expr with
    // literal values

    | Int32(i) -> printfn "Integer with value %d" i
    | Double(f)
-> printfn "Floating-point with value %f" f
    | String(s)
-> printfn "String with value %s" s
    // function calls

    | Call(calledOnObject, methInfo, args)
->
        let calledOn = match calledOnObject with
                       | Some(x) -> sprintf "%A" x
                       | None
-> "(Called a static method)"
        printfn "Calling method '%s': \n\
                 On value: %s \n\
                 With args: %A" methInfo.Name calledOn args
    | _
-> printfn "Unknown expression form:\n%A" expr

 사용예를 보자: 

 val describeCode : Quotations.Expr -> unit

> describeCode <@ 27 @>;;
Integer with value 27
val it : unit = ()

> describeCode <@ 1.0 + 2.0 @>;;
Calling method 'op_Addition':
On value: (Called a static method)
With args: [Value (1.0); Value (2.0)]
val it : unit = ()

두번째 예를 다시 보자. <@ 1.0 + 2.0 @>은 다음의 값을 갖는다: 

 > <@@ 1.0 + 2.0 @@>;;
val it : Expr = Call (None, op_Addition, [Value (1.0), Value (2.0)])
{CustomAttributes = [NewTuple (Value ("DebugRange"), NewTuple (Value ("stdin"), Value (6), Value (4), Value (6), Value (13)))]; Type = System.Double;}

Expr.Call은 Expr * MethodInfo * Expr list -> Expr 타입을 갖는다. 첫번째 인자는 메소드가 불리우는 객체가 되는데, 스태틱 메소드의 경우는 None이 된다. 두번째 인자는 메소드에 대한 정보가 있고 마지막 인자는 메소드에 넘겨줄 인자들이 된다.

다음예는 객체 메소드를 호출하는 경우이다.

> let x = "S";;
val x : string = "S"

> describeCode <@ x.ToUpper() @>;;
Calling method 'ToUpper':
On value: PropertyGet (None, x, [])
With args: []
val it : unit = ()

op_Addition같은 오퍼레이터를 쉽게 매칭할 수 있도록 SpecifiCall이라는 active pattern이 정의되어 있다. 예를 통해서 SpecificCall의 사용법을 살펴 보자: 

let mul2 operation =
    match operation with
    | SpecificCall <@ (*) @> (_, _, [Int32(0); _])
    | SpecificCall <@ (*) @> (_, _, [_; Int32(0)])
-> <@ 0 @>
    | SpecificCall <@ (*) @> (_, _, [Int32(a); Int32(b)])
-> <@ a * b @>
    | _
-> failwith "Unknown quotation form."

> mul2 <@ 1 * 2 @>;;
val it : Expr<int> = Call (None, op_Multiply, [Value (1), Value (2)])

> mul2 <@ 2 * 0 @>;;
val it : Expr<int> = Value (0)

 

 

 

 

 

Posted by PLTeacher