Search code examples
f#expressionhyprlinkr

How do I supply an Expression<Action<T>> in F# when the method has a return value?


I'm attempting to convert some C# code to F#. Specifically, I'm attempting to convert some code using Hyprlinkr to F#.

The C# code looks like this:

Href = this.linker.GetUri<ImagesController>(c =>
    c.Get("{file-name}")).ToString()

where the GetUri method is defined as

public Uri GetUri<T>(Expression<Action<T>> method);

and ImagesController.Get is defined as

public HttpResponseMessage Get(string id)

In F#, I'm attempting to do this:

Href = linker.GetUri<ImagesController>(
    fun c -> c.Get("{file-name}") |> ignore).ToString())

This compiles, but at run-time throws this exception:

System.ArgumentException was unhandled by user code
HResult=-2147024809
Message=Expression of type 'System.Void' cannot be used for return type 'Microsoft.FSharp.Core.Unit'
Source=System.Core

As far as I understand this, the F# expression is an expression that returns unit, but it should really be an Expression<Action<T>>, 'returning' void.

I'm using F# 3.0 (I think - I'm using Visual Studio 2012).

How can I address this problem?


Solution

  • My guess is that it should be fixed in F# 3.1. This is from VS2013 Preview

    type T = static member Get(e : System.Linq.Expressions.Expression<System.Action<'T>>) = e
    type U = member this.MakeString() = "123"
    T.Get(fun (u : U) -> ignore(u.MakeString())) // u => Ignore(u.MakeString())
    

    UPDATE: Cannot check with actual library from the question, so I'd try to mimic the interface I see. This code works fine in F# 3.1

    open System
    open System.Linq.Expressions
    
    type Linker() = 
        member this.GetUri<'T>(action : Expression<Action<'T>>) : string = action.ToString()
    
    type Model() = class end
    
    type Controller() = 
        member this.Get(s : string) = Model()
    
    let linker = Linker()
    let text1 = linker.GetUri<Controller>(fun c -> c.Get("x") |> ignore) // c => op_PipeRight(c.Get("x"), ToFSharpFunc(value => Ignore(value)))
    let text2 = linker.GetUri<Controller>(fun c -> ignore(c.Get("x"))) // c => Ignore(c.Get("x"))
    
    printfn "Ok"
    

    UPDATE 2: I've peeked into the source code of Hyprlinkr and I guess I've found the reason. Current implementation of library code that analyzes expression trees is making certain assumptions about its shape. In particular:

    // C#
    linker.GetUri((c : Controller) => c.Get("{file-name}"))
    
    1. Code assumes that the body of expression tree is method call expression (i.e. invokation of some method from controller)
    2. Then code picks method call arguments one by one and tries to get its values by wraping them into 0-argument lambda, compiling and running it. Library implicitly relies that argument values are either constant values or values captured from the enclosing environment.

    Shape of expression tree generated by F# runtime (i.e. when piping is used) will be

    c => op_PipeRight(c.Get("x"), ToFSharpFunc(value => Ignore(value)))

    This is still method call expression (so assumption 1 will still be correct) but its first argument uses parameter c. If this argument will be converted to lambda with no arguments (() => c.Get("x")) - then method body of such lambda will refer to some free variable c - precisely what was written in exception message.

    As an alternative that will be more F# friendly I can suggest to add extra overload for GetUri

    public string GetUri<T, R>(Expression<Func<T, R>> e)
    

    It can be both used on C# and F# sides

    // C#
    linker.GetUri((Controller c) => c.Get("{filename}"))
    
    // F#
    linker.GetUri(fun (c : Controller) -> c.Get("{filename}"))