Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Evaluate function inside quotation

Tags:

f#

I'm at the moment doing some very basic pattern matching with quotations.

My code:

let rec test e =
    match e with
    | Patterns.Lambda(v,e) -> test e
    | Patterns.Call(_, mi, [P.Value(value, _); P.Value(value2, _)]) -> 
        printfn "Value1: %A | Value2 : %A" value value2
    | Patterns.Call(_, mi, [P.Value(value, _); P.PropertyGet(_, pi, exprs)]) ->
        printfn "Value1: %A | Value2 : %A" value (pi.GetValue(pi, null))
    | _ -> failwith "Expression not supported"


let quot1 = <@ "Name" = "MyName" @>
(* Call (None, Boolean op_Equality[String](System.String, System.String),
      [Value ("Name"), Value ("lol")]) *)

let quot2 = <@ "Name" = getNameById 5 @>
(* Call (None, Boolean op_Equality[String](System.String, System.String),
      [Value ("Name"),
       Call (None, System.String getNameById[Int32](Int32), [Value (5)])]) *)

test quot1 // Works!
test quot2 // Fails.. Dosent match any of the patterns.

Is it possible to somehow evaluate the result of the getNameById function first, so that it will match one of the patterns, or am I doomed to assign a let binding with the result of the function outside the quotation?

I've tried playing with the ExprShape patterns, but without luck..

like image 359
ebb Avatar asked Jun 19 '11 16:06

ebb


2 Answers

You can use PowerPack's Eval to evaluate only the arguments to the Call expression:

match e with
| Call(_,mi,[arg1;arg2]) ->
  let arg1Value, arg2Value = arg1.Eval(), arg2.Eval()
  ...

And similarly for Lambda expressions, etc. Noticed this frees you from enumerating permutations of Value, Property, and other argument expressions.

Update

Since you want to avoid using Eval (for good reason if you are implementing a performance conscious application), you'll need to implement your own eval function using reflection (which is still not lightening fast, but should be faster than PowerPack's Eval which involves an intermediate translation of F# Quotations to Linq Expressions). You can get started by supporting a basic set of expressions, and expand from there as needed. Recursion is the key, the following can help you get started:

open Microsoft.FSharp.Quotations
open System.Reflection

let rec eval expr =
    match expr with
    | Patterns.Value(value,_) -> value //value
    | Patterns.PropertyGet(Some(instance), pi, args) -> //instance property get
        pi.GetValue(eval instance, evalAll args) //notice recursive eval of instance expression and arg expressions
    | Patterns.PropertyGet(None, pi, args) -> //static property get
        pi.GetValue(null, evalAll args)
    | Patterns.Call(Some(instance), mi, args) -> //instance call
        mi.Invoke(eval instance, evalAll args)
    | Patterns.Call(None, mi, args) -> //static call
        mi.Invoke(null, evalAll args)
    | _ -> failwith "invalid expression"
and evalAll exprs =
    exprs |> Seq.map eval |> Seq.toArray

And then wrapping this in an Active Pattern will improve syntax:

let (|Eval|) expr =
    eval expr

match e with 
| Patterns.Call(_, mi, [Eval(arg1Value); Eval(arg2Value)]) -> ...

Update 2

OK, this thread got me motivated to try and implement a robust reflection based solution, and I've done so with good results which are now part of Unquote as of version 2.0.0.

It turned out not to be as difficult as I thought it would be, currently I am supporting all quotation expressions except for AddressGet, AddressSet, and NewDelegate. This is already better than PowerPack's eval, which doesn't support PropertySet, VarSet, FieldSet, WhileLoop, ForIntegerRangeLoop, and Quote for example.

Some noteworthy implementation details are with VarSet and VarGet, where I need to pass around an environment name / variable lookup list to each recursive call. It is really an excellent example of the beauty of functional programming with immutable data-structures.

Also noteworthy is special care taken with issues surrounding exceptions: striping the TargetInvokationExceptions thrown by reflection when it catches exceptions coming from methods it is invoking (this is very important for handling TryWith evaluation properly, and also makes for better user handling of exceptions which fly out of the quotation evaluation.

Perhaps the most "difficult" implementation detail, or really the most grueling, was the need to implement all of the core operators (well, as most I could discover: the numeric and conversion operators, checked versions as well) since most of them are not given dynamic implementations in the F# library (they are implemented using static type tests with no fallback dynamic implementations), but also means a serious performance increase when using these functions.

Some informal benchmarking I observe performance increases of up to 50 times over PowerPack's (not pre-compiled) eval.

I am also confident that my reflection-based solution will be less bug prone then PowerPack's, simply because it is less complicated than the PowerPack's approach (not to mention I've backed it up with about 150 unit tests, duly fortified by Unquotes additional 200+ unit tests which now is driven by this eval implementation).

If you want to peek at the source code, the main modules are Evaluation.fs and DynamicOperators.fs (I've locked the links into revision 257). Feel free to grab and use the source code for your own purposes, it licensed under Apache License 2.0! Or you could wait a week or so, when I release Unquote 2.0.0 which will include evaluation operators and extensions publicly.

like image 181
Stephen Swensen Avatar answered Sep 27 '22 23:09

Stephen Swensen


You can write an interpreter that will evaluate the quotation and call the getNameById function using Reflection. However, that would be quite a lot of work. The ExprShape isn't going to help you much - it is useful for simple traversing of quotations, but to write an interpreter, you'll need to cover all patterns.

I think the easiest option is to evaluate quotations using the PowerPack support:

#r "FSharp.PowerPack.Linq.dll"

open Microsoft.FSharp.Linq.QuotationEvaluation

let getNameById n = 
  if n = 5 then "Name" else "Foo"

let quot1 = <@ "Name" = "MyName" @>
let quot2 = <@ "Name" = getNameById 5 @>

quot1.Eval()    
quot2.Eval()    

This has some limitations, but it is really the easiest option. However, I'm not really sure what are you trying to achieve. If you could clarify that, then you may get a better answer.

like image 41
Tomas Petricek Avatar answered Sep 27 '22 22:09

Tomas Petricek