Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 758
Mark Seemann Avatar asked Jul 17 '13 07:07

Mark Seemann


3 Answers

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}"))
like image 53
desco Avatar answered Oct 08 '22 23:10

desco


As a workaround for F# 2.0, you can define your own "ignore" function with a generic return type. This apparently allows void to be inferred.

let noop _ = Unchecked.defaultof<_>

Href = linker.GetUri<ImagesController>(fun c -> 
    c.Get("{file-name}") |> noop).ToString())
like image 43
Daniel Avatar answered Oct 08 '22 23:10

Daniel


In this case, I think that you may be able to just call ignore without using a pipe:

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

UPDATE

Given desco's diagnosis of HyprLinkr's behavior, it seems like you ought to be able to use a utility along these lines:

open System
open System.Linq.Expressions

type ActionHelper =
    static member IgnoreResult(e:Expression<Converter<'t,_>>) = 
        Expression.Lambda<Action<'t>>(e.Body, e.Parameters) 

Then you can do

Href = linker.GetUri<ImagesController>(
    ActionHelper.IgnoreResult(fun c -> c.Get("{file-name}"))).ToString()
like image 1
kvb Avatar answered Oct 08 '22 22:10

kvb