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

3

7

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?

Sorensen answered 17/7, 2013 at 7:1 Comment(2)
Pretty sure this is a known bug - could you try it with F# 3.1 in the Visual Studio 2013 preview to see if it still repros?Cassareep
See my comment to desco's answer.Sorensen
F
3

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}"))
Foxtrot answered 17/7, 2013 at 14:11 Comment(6)
It looks like the actual issue may be with piping, not with ignore per se. Does piping to ignore also work with F# 3.1?Cassareep
@kvb: You're right, ignore(...) works in 3.0 too. Now I'm curious why the pipe in my answer works.Invalid
Actually my original answer was with piping (and it works too). I've modified it to get better represented result expression treeFoxtrot
In F# 3.1, I'm now consistently getting the same error with or without piping: "variable 'c' of type 'ImagesController' referenced from scope '', but it is not defined". In F# 3.0, I only got that error when not using piping.Sorensen
Update 2 in the answerFoxtrot
Thanks for your help! Fortunately, I also control Hyprlinkr, so I added an overload as you suggested, and that solved the issue.Sorensen
I
1

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())
Invalid answered 17/7, 2013 at 14:28 Comment(3)
It's not that void can be inferred; it's that typically functions that return unit (including ignore) are actually compiled to void returning methods; for generic return types this can't be the case (because void is not a legal type argument in the .NET type system).Cassareep
Yeah, 'inferred' wasn't the right word. But it does create a lambda expression with a void return type.Invalid
That gives me another run-time exception: "variable 'c' of type 'ImagesController' referenced from scope '', but it is not defined"Sorensen
C
1

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()
Cassareep answered 17/7, 2013 at 15:23 Comment(2)
That gives me another run-time exception: "variable 'c' of type 'ImagesController' referenced from scope '', but it is not defined"Sorensen
@MarkSeemann - can you include the stack trace? Is that exception being thrown by the F# runtime or by Hyprlinkr (or something else)?Cassareep

© 2022 - 2024 — McMap. All rights reserved.