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?
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}"))
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}"))
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())
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With