How do you create F# anonymous records in C#?
Asked Answered
K

1

12

I can see that if I create a new anonymous record, eg.

let myRecord = {| SomeInteger = 5 |}

then if it's exposed to C# then I can dot into it with

var someInteger = myRecord.SomeInteger;

What about the other way round, if I have an F# function, say:

let unwrap (record : {| SomeInteger : int |}) = record.SomeInteger

and it's exposed to C#, how can I instantiate an argument for this function from C# and call it? I tried naively just placing a C# anonymous type there, ie.

var unwrapped = unwrap(new { SomeInteger = 5 });

but this didn't compile. I note in the RFC for the feature it's said that "The feature must achieve compatibility with C# anonymous objects (from C# 3.0)" but it's not specifically mentioned in which ways. Is this supported?

Komsomol answered 4/4, 2019 at 20:14 Comment(4)
"no additional detail is given"? The RFC explains what the compatibility requirement refers to directly in the lines following that statement. There is no way to create instances of such a type if you can't take advantage of F# type inference, because as anonymous types they have no real name by which to refer to them, and C# has no way to use anonymous types beyond the method scope.Haug
I suggest opening the resulting F#-built assembly in ILSpy (or whichever such tool you prefer) and looking at the type that's generated for myRecord. That should tell you a lot about the way in which that compatibility was realized.Conclusive
Yeah I was a bit vague there, I'll update the question. Perhaps I'm misunderstanding but the lines afterwards appear to me to describe what the implementation has in common with C# anonymous types rather than how they interop. I'm not sure if type inference is the issue here - the C# compiler could read the generated type and know how to compile a matching anonymous type or ValueTuple literal to it (ok so I guess this wouldn't be an F# feature as such tbf). I love the feature but it seems a bit strange for "Interop" to be a design goal if we can't use a function that receives them from C#.Komsomol
I'm not sure what the scope of "interop" is in this case. The thing is: Anonymous types in C# are created solely based on type inference within a method scope, and they have no names. To create an instance of a type in C#, you either need its name/constructor, or it must be a local anonymous type. The capabilities of C# will and can not surpass this for anonymous F# types - they still have no name you can use to create them. What you can do is return an anonymous record from an F# function and bind that to a variable in C# with var and access its fields.Haug
L
2

Unfortunately, it seems to be basically impossible. We can use https://sharplab.io/ to have a look what is going on and how will the API look like.

You can have a look at the example at: https://sharplab.io/#v2:EYLgxg9gTgpgtADwGwBYA0AbEAzAzgHwFgAoDGAFwAIBbATwCUZIoATSgXkoG99KBlCNRgBJAHbkYAcxhQOlAKyV8AXxJkqAV1EB3KAEMADgB4A5HoB8lABSxmbEN14ChYidNkOzS5QEo5t6BYAOmcRcSkZEiA==

I have pasted following F# into it, let it compile and then decompile as C# code:

let createRecord () = {| SomeInteger = 5 |}
let unwrap (record : {| SomeInteger : int |}) = record.SomeInteger

We can see that the F# compiler generated class named <>f__AnonymousType2453178905. The name is the major problem, as you can't reference it in C# :/ (AFAIK). Btw, it is interesting that the type of SomeInteger is generic, so you can write the unwrap function generic and it will still work:

let unwrap<'a> (record : {| SomeInteger : 'a |}) = record.SomeInteger

The translated function look like this

public static <>f__AnonymousType2453178905<int> createRecord()
{
    return new <>f__AnonymousType2453178905<int>(5);
}
public static int unwrap(<>f__AnonymousType2453178905<int> record)
{
    return record.SomeInteger;
}

That means:

  • I'd avoid using anonymous records in public API
  • If you really want to use them, you'll have to provide factory functions for them.
    • they can be generic in all parameters, for example let createMyRecord a = {| SomeInteger = a |} will work well
    • while it will be usable from C#, the seeing that the parameter should be of type <>f__AnonymousType3239938913<<A>j__TPar, <B>j__TPar, <SomeInteger>j__TPar> will not be very helpful
  • If somebody else has built the API and now you have to use, you can still create the instance using reflection
    • you can get the type from a MethodInfo by calling method.GetParameters()[0].ParameterType or by Type.GetType("namespace.typenamenumberOfGenericArgs")
    • the type has a constructor, the properties are there in alphabetic order. You can use Activator.CreateInstance(...) to call it.
Lengthy answered 4/11, 2019 at 11:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.