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?

Mark Seemann
  • 225,310
  • 48
  • 427
  • 736

3 Answers3

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}"))
desco
  • 16,642
  • 1
  • 45
  • 56
  • 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? – kvb Jul 17 '13 at 16:16
  • @kvb: You're right, `ignore(...)` works in 3.0 too. Now I'm curious why the pipe in my answer works. – Daniel Jul 17 '13 at 16:36
  • Actually my original answer was with piping (and it works too). I've modified it to get better represented result expression tree – desco Jul 17 '13 at 17:43
  • 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. – Mark Seemann Jul 18 '13 at 08:26
  • Update 2 in the answer – desco Jul 18 '13 at 21:49
  • Thanks for your help! Fortunately, I also control Hyprlinkr, so I added an overload as you suggested, and that solved the issue. – Mark Seemann Jul 23 '13 at 08:36
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())
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • 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). – kvb Jul 17 '13 at 16:00
  • Yeah, 'inferred' wasn't the right word. But it does create a lambda expression with a `void` return type. – Daniel Jul 17 '13 at 16:03
  • That gives me another run-time exception: "variable 'c' of type 'ImagesController' referenced from scope '', but it is not defined" – Mark Seemann Jul 18 '13 at 06:38
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()
kvb
  • 54,864
  • 2
  • 91
  • 133
  • That gives me another run-time exception: "variable 'c' of type 'ImagesController' referenced from scope '', but it is not defined" – Mark Seemann Jul 18 '13 at 06:35
  • @MarkSeemann - can you include the stack trace? Is that exception being thrown by the F# runtime or by Hyprlinkr (or something else)? – kvb Jul 18 '13 at 12:18