Skip to content

Instantly share code, notes, and snippets.

@htsign
Last active February 19, 2018 13:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save htsign/f6cc6608cc2cc056704c8e12bda7114c to your computer and use it in GitHub Desktop.
Save htsign/f6cc6608cc2cc056704c8e12bda7114c to your computer and use it in GitHub Desktop.
引用式を評価して、式の途中で null になった場合に全体として null を返す関数を定義したい人生だった。C# でいうnull条件演算子( ?. ) のようなものを作りたいので少しずつ実装を進める。つもり。
namespace QuotationUtility
open System.Reflection
open Microsoft.FSharp.Linq.RuntimeHelpers
open Microsoft.FSharp.Quotations
open Microsoft.FSharp.Quotations.Patterns
module Option =
let toObj = function None -> null | Some x -> x
module Quotation =
type ExprInfo = Expr option * MemberInfo * Expr list
let evaluate (expr : Expr<'a> when 'a : null) : 'a =
let eval = LeafExpressionConverter.EvaluateQuotation
// 引用式の解析を行う
let rec analyzeExpr (acc : ExprInfo list) = function
| None -> acc
| Some expr ->
// インスタンスが存在する限り、再帰的に式を潜る
match expr with
| PropertyGet (parent, pi, args) ->
analyzeExpr ((parent, pi :> MemberInfo, args) :: acc) parent
| Call (parent, mi, args) ->
analyzeExpr ((parent, mi :> MemberInfo, args) :: acc) parent
| FieldGet (parent, fi) ->
analyzeExpr ((parent, fi :> MemberInfo, []) :: acc) parent
| NewObject (ci, args) ->
analyzeExpr ((None, ci :> MemberInfo, args) :: acc) None
// 定数が与えられた場合は MemberInfo を null にして invoke 内で直接返すことにしてお茶を濁す
| Value (value, _) -> (Some <@@ value @@>, null, []) :: []
| Application (_, value) -> (Some value, null, []) :: []
| Lambda (_, expr) -> analyzeExpr acc (Some expr)
| _ -> invalidArg "expr" (sprintf "invalid quotation: %A" expr)
let invoke (ins : obj) (mi : MemberInfo) (args : obj []) =
match mi with
// expr が Value or Application にマッチした場合に評価される
| null -> ins
| :? PropertyInfo as pi -> pi.GetValue(ins, args)
| :? MethodInfo as mi -> mi.Invoke(ins, args)
| :? FieldInfo as fi -> fi.GetValue ins
| :? ConstructorInfo as ci -> ci.Invoke args
| _ -> invalidArg "mi" "unsupported type"
let infos = analyzeExpr [] (Some (expr :> Expr))
let rec exec = function
| [] -> failwith "invalid quotation"
| (ins, mi, args) :: xs ->
let ins = ins |> Option.map eval |> Option.toObj
let args = args |> List.map eval |> List.toArray
match invoke ins mi args, xs with
| null, _ -> null
| value, [] -> value
| ins, (_, mi, args) :: xs -> exec ((Some <@@ ins @@>, mi, args) :: xs)
exec infos :?> 'a
@htsign
Copy link
Author

htsign commented Feb 16, 2018

open System
open QuotationUtility

let nullUri : Uri = null
let result1 = Quotation.evaluate <@ "not null value" @>
let result2 = Quotation.evaluate <@ (Uri("http://example.com/")).AbsoluteUri @>
let result3 = Quotation.evaluate <@ (null : Uri).AbsoluteUri @>
let result4 = Quotation.evaluate <@ nullUri.AbsoluteUri @>

printfn "1 : [%A] / 2 : [%A] / 3 : [%A] / 4 : [%A]" result1 result2 result3 result4
(* 1 : ["not null value"] / 2 : ["http://example.com/"] / 3 : [<null>] / 4 : [<null>] *)

@htsign
Copy link
Author

htsign commented Feb 16, 2018

テストしてないけど、インスタンスが存在することを前提に組んでいるので、たぶんスタティックメソッドを呼んだ時点で死ぬと思う。

@htsign
Copy link
Author

htsign commented Feb 18, 2018

上でやってることと変わらないけど、メソッドチェーンが長くなっても問題ない…はず。

open QuotationUtility

type C() =
    member __.D = "This is a property 'D'"
type B() =
    member __.C = C()
type A() =
    member __.B1 = B()
    member __.B2 = Unchecked.defaultof<B>

let result1 = Quotation.evaluate <@ A().B1.C.D @>
let result2 = Quotation.evaluate <@ A().B2.C.D @>

printfn "1 : [%A] / 2 : [%A]" result1 result2
(* 1 : ["This is property 'D'"] / 2 : [<null>] *)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment