3
votes

This code snippet reproduces a problem I am having with some production code. The function containsProperty represents a real world function that is actually in a library, so that I have no say in what the signature is.

The problem is that I can't figure out how to create a wrapper function that can take a normal function as argument, and then pass that on to containsProperty. I can call containsProperty directly with a function as a lambda expression, but I can't call it with a function that comes from some other source.

The function addToGroup is the best I've come up with so far, and it uses quotations. There are two problems with that approach, which I am trying to figure out. First, how do I get rid of the Func cast in the quotation? Perhaps somehow move it into addToGroup? Second, can I build on this in order to just pass a function? I haven't succeeded in finding something that doesn't produce either a compile time error or a runtime error.

The function addToGroup2 is what I'd like to do, but it doesn't compile. The error message is "No constructors are available for the type 'Quotations.Expr<'a>'".

Why do I bother to struggle with this? Because as long as I can't treat the passed in function as a first class value, I can't create the design I'm after. I want these functions to come along from a collection of records.

If you paste this snippet into LINQPad or something, comment out addToGroup2 and the calls to it, in order to make the snippet compile and run.

open System
open System.ComponentModel
open System.ComponentModel.DataAnnotations // Reference to this assembly required.

type CfgSettings = {
    mutable ConnectionString: string
    mutable Port: int
    }

and CfgSettingsMetadata() =
    static member containsProperty<'TProperty>(propertyExpression: Linq.Expressions.Expression<Func<CfgSettings,'TProperty>>) =
        Console.WriteLine "good!"
    static member addToGroup f =
        CfgSettingsMetadata.containsProperty(FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToLambdaExpression f) |> ignore
    static member addToGroup2 (f: CfgSettings -> 'TProperty) =
        CfgSettingsMetadata.containsProperty(FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToLambdaExpression (Quotations.Expr<Func<CfgSettings,'TProperty>>f)) |> ignore
    static member BuildMetadata () =
        CfgSettingsMetadata.containsProperty(fun x -> x.ConnectionString)
        CfgSettingsMetadata.containsProperty(fun x -> x.Port)
        CfgSettingsMetadata.addToGroup <@ Func<_,_>(fun x -> x.ConnectionString) @>
        CfgSettingsMetadata.addToGroup <@ Func<_,_>(fun x -> x.Port) @>
        CfgSettingsMetadata.addToGroup2 (fun x -> x.ConnectionString)
        CfgSettingsMetadata.addToGroup2 (fun x -> x.Port)

CfgSettingsMetadata.BuildMetadata()

Both answers in question Expression<Func<T, bool>> from a F# func helped me somewhat, but I haven't found a solution yet.

1

1 Answers

5
votes

So, there are two questions here.

How to pass a function without having to wrap it in <@ ... @>?

For this, you just need to add the [<ReflectedDefinition>] attribute to your method's parameter. It implicitly wraps the argument passed to it in a quotation.

type CfgSettingsMetadata() =
    static member addToGroup([<ReflectedDefinition>] f: Expr<CfgSettings -> 'TProperty>) =
        CfgSettingsMetadata.containsProperty(LeafExpressionConverter.QuotationToLambdaExpression f) |> ignore

// Example use:
CfgSettingsMetadata.addToGroup(Func<_, _>(fun x -> x.ConnectionString))

How to convert from Expr<a -> b> to Expression<Func<a, b>>?

This is indeed explained in the question you linked, although the API has changed a bit since then.

type CfgSettingsMetadata() =
    static member addToGroup ([<ReflectedDefinition>] (f: Expr<CfgSettings -> 'TProperty>)) =
        let call = LeafExpressionConverter.QuotationToExpression f :?> MethodCallExpression
        let lambda = call.Arguments.[0] :?> LambdaExpression
        let e = Expression.Lambda<Func<CfgSettings, 'TProperty>>(lambda.Body, lambda.Parameters)
        CfgSettingsMetadata.containsProperty(e) |> ignore

// Example use:
CfgSettingsMetadata.addToGroup(fun x -> x.ConnectionString)