A collection of utilities for building, inspecting, transforming and executing F# quotations.
Install from nuget at https://www.nuget.org/packages/Qit/
#r "nuget:Qit"
Splicing operators !% and !%% combined with Quote.expandOperators extend what can be spliced. Consider the quoted function f:
open Qit
open FSharp.Quotations
type MyType = |A of int | B of double
let f =
<@
fun x ->
match x with
| A v -> A(v + 1)
| B v -> B(v + 1.0)
@>Now, a bit contrived, but lets say we need to use the Tag property of MyType in a quoted expression. We can't just use %(Expr.PropertyGet(<@x@>, pinfo)) because x is bound in the quotation. We can use !% to splice in the expression x and then use Quote.expandOperators to expand the spliced expression:
let f2 =
<@
fun (x : MyType) ->
let tag = !%%(Expr.PropertyGet(<@x@>, typeof<MyType>.GetProperty("Tag")))
printfn "Tag: %d" tag
match x with
| A v -> A(v + 1)
| B v -> B(v + 1.0)
@>
|> Quote.expandOperators
<@ (%f2) (A 1) @> |> Quote.evaluateTag: 0
A 2
Note that !% and !%% are also available as functions QitOp.splice and QitOp.spliceUntyped respectively.
Simplified pattern matching:
let uselessIf =
<@
let a = 34
if true then
a + 1
else
a - 2
@>
let rec removeTrivialIfs expr =
expr
|> Quote.traverse
(fun q ->
match q with
| BindQuote <@if true then Quote.any "body" else Quote.any "" @>
(Marker "body" body)
| BindQuote <@if false then Quote.any "" else Quote.any "body" @>
(Marker "body" body) ->
Some(removeTrivialIfs body)
| _ -> None
)Quote.any is matching any expression and binding it to a name which can then be extracted with the Marker pattern. Quote.traverse traverses the quotation and applies the given function to each subexpression optionally replacing it with the result of the function.
If we simply wanted to match with no binding we could have done
uselessIf
|> Quote.exists
(function
| Quote <@if true then Quote.any "body" else Quote.any "" @> ->
true
| _ -> false)true
Again, Quote.any is matching any expression, but what about an expression of specific type? For that there's Quote.withType<'t>:
uselessIf
|> Quote.exists
(function
| Quote <@if true then Quote.withType<int> "" else Quote.withType<int> "" @> ->
true
| _ -> false)true
uselessIf
|> Quote.exists
(function
|Quote <@if true then Quote.withType<double> "" else Quote.withType<double> "" @> ->
true
| _ -> false)false
When binding to markers , the Marker pattern will extract both Quote.any and Quote.withType bindings. To be specific we could use the AnyMarker and TypedMarker patterns.
We used an empty string to signify that we're not interested in binding the matched expression. When specifying a name we expect bindings with the same name to be equivalent.
uselessIf
|> Quote.exists
(function
| Quote <@if true then Quote.withType<int> "myMarker" else Quote.withType<int> "myMarker" @> ->
true
| _ -> false)false
Looking at uselessIf we can see that the true/false cases are different and so the match fails. Now when they're the same:
let uselessIf2 =
<@
let a = 32
if a > 100 then
a + 1
else
a + 1
@>
uselessIf2
|> Quote.exists
(function
| Quote <@
if Quote.withType"" then
Quote.withType<int> "myMarker"
else Quote.withType<int> "myMarker"
@> -> true
| _ -> false)true
Variable names must also match unless prefixed with __.
uselessIf
|> Quote.exists
(function
| Quote <@let a = Quote.any "" in Quote.any ""@> -> true
| _ -> false)true
Original code has let a and so it matches.
uselessIf
|> Quote.exists
(function
|Quote <@let b = Quote.any "" in Quote.any ""@> -> true
| _ -> false)false
Original code has let a which does not match let b
uselessIf
|> Quote.exists
(function
| Quote <@let __b = Quote.any "" in Quote.any ""@> -> true
| _ -> false)true
Now that we've prefixed the variable name with __, the name of the variable is no longer relevant and it matches.
As motivation say we have,
let q0 =
<@
let a = ResizeArray()
a.Add 10
a.Add 20
a.Add 30
a.ToArray()
@>and we want to replace the ResizeArray.Add method calls with ResizeArray.AddRange([arg]). The strategy encouraged here is to create a function with type parameters, use reflection once to apply the Type arguments, and invoke.
So we create a function with a type parameter that is otherwise not present in the function signature.
let addRange0<'a> (o : Expr) (arg : Expr) =
let ra : 'a ResizeArray Expr = o |> Expr.Cast
let arg : 'a Expr = arg |> Expr.Cast
<@@ (%ra).AddRange([%arg]) @@>val addRange0<'a> : o: Expr -> arg: Expr -> Expr
We can then use TypeTemplate.create to create a function that takes Type arguments and applies them to addRange0.
let addRange = TypeTemplate.create addRange0
// addRange : Type list -> Expr -> Expr -> ExprNow instead of the typical reflection mess we can just call the new addRange function. The call itself is a compiled expression which is cached on the type arguments given, reducing call overhead compared to MethodInfo.Invoke. Note: We're also introducing the operator equivalents to Quote.any (!@@) and Quote.withType (!@).
let result =
q0
|> Quote.traverse
(fun q ->
match q with
| BindQuote <@ (!@"ra" : _ ResizeArray).Add(!@@"v1") @>
(Marker "ra" ra & Marker "v1" v1) ->
Some(addRange [v1.Type] ra v1)
| _ -> None
)
// Did this do what we wanted?
let expected =
<@
let a = ResizeArray()
a.AddRange [10]
a.AddRange [20]
a.AddRange [30]
a.ToArray()
@>
Quote.isEquivalent result expectedtrue
If we wanted to inline this concept and forgo the caching provided by TypeTemplate.create, ITypeTemplate<'ReturnType>.Def<'typeParameters> can be used. Here 'typeParameters would be either a single type or a tuple representing the needed type parameters. The Make method is used to apply the type parameters.
q0
|> Quote.traverse
(fun q ->
match q with
| BindQuote <@ (Quote.withType<AnyType ResizeArray> "ra").Add(Quote.any "v1") @>
(Marker "ra" ra & Marker "v1" v1) ->
{ new ITypeTemplate<Expr> with
member _.Def<'a>() = <@@ (%%ra : 'a ResizeArray).AddRange([%%v1]) @@>
}.Make [v1.Type]
|> Some
| _ -> None
)Qit references FSharp.Quotations.Evaluator and
includes FSharp.TypeProviders.SDK
to allow for evaluation of F# quotations. Quote.evaluate and Quote.evaluateUntyped
uses FSharp.Quotations.Evaluator for evaluation.
<@ 1 + 2 @> |> Quote.evaluate 3
<@@ 1 + 2 @@> |> Quote.evaluateUntyped 3
The Qit.ProviderImplementation namespace is a very slighty modified version of the type provider sdk. Quote.compileLambda will
generate an assembly with a method generated from the Expr. It then retrieves the method and builds an F# function using FSharp.Linq.RuntimeHelpers.LeafExpressionConverter
that calls the method.
let myFunction = <@ fun () -> 1 + 2 @> |> Quote.compileLambda
myFunction()3
The full type provider sdk can be used to generate assemblies from quotations at runtime.