Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# code invoking a c# method containing a Func parameter behaving strangely

Tags:

c#

f#

c#-to-f#

We came across an issue recently with F# code calling into C# code. I have simplified the issue down as simple as I can. The C# code is as follows:

using System;

namespace CSharpLib
{
    public abstract class InputMessageParent
    {
        public readonly Guid Id;

        protected InputMessageParent(Guid id) { this.Id = id; }
    }

    public class Result { public string ResultString; }

    public enum FunctionResult
    {
        None,
        Good,
        Bad
    }

    public class ConfigurationBuilder
    {
        public Result DoWork<TMessage>(
            string param1,
            Func<TMessage, FunctionResult> action)
            where TMessage : InputMessageParent
        {
            return new Result() { ResultString = "Good" };
        }
    }
}

The F# code needs to call the ConfigurationBuilders DoWork function. Note that the DoWork function takes two parameters, a simple string and a Func as a second parameter. The Func takes in a TMessage which must inherit from InputMessageParent and it returns a simple Result type. The F# code is as follows:

open System

type InputMessageConcreteTypeA =
    inherit CSharpLib.InputMessageParent
    val Property1:string
    new (id, property1Value) =
        {
            inherit CSharpLib.InputMessageParent(id)
            Property1 = property1Value
        }

[<EntryPoint>]
let main argv =

    let actionImpl (input:InputMessageConcreteTypeA) =
    CSharpLib.FunctionResult.Good

    let builder = new CSharpLib.ConfigurationBuilder()
    builder.DoWork("param1", actionImpl) |> ignore

    0

This code does not compile. The actionImpl type is InputMessageConcreteTypeA -> CSharpLib.FunctionResult, this is exactly the type of the second parameter that DoWork is expecting but instead it gives me the following error: This expression was expected to have type 'Func<'a,CSharpLib.FunctionResult>' but here has type 'b -> CSharpLib.FunctionResult'

Interestingly, if I change the code to the following its does compile:

[<EntryPoint>]
let main argv =

let actionImpl (input:InputMessageConcreteTypeA) =
    CSharpLib.FunctionResult.Good

let builder = new CSharpLib.ConfigurationBuilder()
builder.DoWork("param1", fun input -> actionImpl(input)) |> ignore

0

Why does the code compile for the inline anonymous function which is the exact same type definition as actionImpl but wont compile if I just pass actionImpl directly? The inline anonymous function looks pretty pointless but is the only solution that we have at the moment. Is there a better way?

like image 302
bstack Avatar asked Aug 27 '19 11:08

bstack


1 Answers

The F# function type 'a -> 'b is not, in fact, the same as the C# type Func<a,b>.

The reason for why they're different is a bit moot, but the upshot is that you can't just willy-nilly pass one type off as the other. Type mismatch. This is what the compiler is telling you: expected to have type Func<...>, but here has type 'b -> ...

For lambda expressions, however, the compiler makes an exception. Normally, a lambda expression fun x -> e will have type 'a -> 'b (where x:'a and e:'b), but in cases where it's already known from the context that the expected type is Func<_,_> the compiler will oblige and compile the lambda expression as a Func.

This exception was made to ease interop with .NET libraries that make heavy use of lambda expressions, such as LINQ. But this exception doesn't apply to referencing named functions by their name. It probably should apply, but it doesn't.

If you don't want to have a pointless lambda expression, the only other way to go is to explicitly create an object of type Func<_,_> by calling its constructor and passing the function to it, then pass that object to DoWork:

builder.DoWork("param1", Func<_,_> actionImpl)
like image 145
Fyodor Soikin Avatar answered Oct 21 '22 20:10

Fyodor Soikin