-
Notifications
You must be signed in to change notification settings - Fork 143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC FS-1030] Discussion: Anonymous record types #170
Comments
I'd like to understand better the motives around two aspects of the RFC:
What are the drivers for each? For the first one, what I find compeling:
Regarding second aspect, I don't really understand the concern of interop with C# anonymous types because in C#, they are always local to a function, and if code returns them or take them as input (that is an idiom in Dapper and ASP.NET MVC), it has to rely on reflection. For me the most natural approach would be to support:
I find possibility to support struct tuple metadata from C# compelling but not the main driver for the feature: I'd not be in a hurry until we see if usage is noticeable in libraries/more code in the wild; also it might fit better an extension to the I'd like to know if type alias will be supported as well. |
Correct, the .NET code we need to interoperate with uses reflection.
yes, |
I've made updates to the RFC, committed direct to master https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1030-anonymous-records.md |
Use Cases for Anonymous ClassesAs a business apps developer, I often use anonymous classes when prototyping ideas or working with code where the structure is ad-hoc and constantly changing. Structured Logging We've recently upgraded projects to start using structured logging libraries like Serilog and NLog, which allows one to log events in a way that saves the events in both a test format and a structured format. For example: Logger.Information("{Name} placed an order of {Total}",
new { Name = customer.Name, Total = order.Total }); Web UI Generation I've written a number of internal components that over the years that allow me to quickly create UI using reflection. For example: @UI.Table(from c in db.Customers
select new { c.Name, Total_Orders = c.Orders.Sum(o => o.Total) }).SetPageSize(20) Will create a pageable and sortable table. Typical Reflection Code I've included a snippet of some typical reflection code I use to extract basic information from a type. Nothing is specific to anonymous classes, but most of the consuming code assumes the classes being reflected adhere to basic POCO standards. public struct MemberGetter {
public string Name;
public Type Type;
public Func<object, object> Get;
}
public MemberGetter[] GetMemberGetters(Type ObjectType) {
var Flags = BindingFlags.Public | BindingFlags.Instance;
var Properties = (
from p in ObjectType.GetProperties((System.Reflection.BindingFlags)Flags)
where p.IsSpecialName == false
where p.GetIndexParameters().Length == 0 && p.CanRead && p.IsSpecialName == false
let param = Expression.Parameter(typeof(object), "x")
let cparam = Expression.Convert(param, ObjectType)
let isType = Expression.TypeIs(param, ObjectType)
let getProp = Expression.Convert(Expression.Property(cparam, p), typeof(object))
let check = Expression.Lambda<Func<object, object>>
(Expression.Condition(isType, getProp, Expression.Constant(null)), param).Compile()
select new MemberGetter {
Name = p.Name,
Type = p.PropertyType,
Get = check
}).ToArray();
var Fields = (
from f in ObjectType.GetFields((System.Reflection.BindingFlags)Flags)
where f.IsPublic && f.IsSpecialName == false
let param = Expression.Parameter(typeof(object), "x")
let cparam = Expression.Convert(param, ObjectType)
let isType = Expression.TypeIs(param, ObjectType)
let getField = Expression.Convert(Expression.Field(cparam, f), typeof(object))
let check = Expression.Lambda<Func<object, object>>(
Expression.Condition(isType, getField, Expression.Constant(null)), param).Compile()
select new MemberGetter {
Name = f.Name,
Type = f.FieldType,
Get = check
}).ToArray();
return Fields.Union(Properties).ToArray();
} |
Optional Naming of Anonymous Types???If we're not going to be restricted to C#'s philosophy of limiting anonymous types to a function scope. What are people's thoughts about being able to assign the type a name when writing a function? For example: open System
let makePerson(name:string, dob:DateTime) : Person = {| Name = name; DOB = dob |} The idea is one use could use the type hint to optionally name the type if there was a need for it. This sort of thing is useful if you're prototyping something like REST endpoint with Swashbuckler/Swagger and these tools require a type name in order to generate the Swagger specification correctly. |
I asked a question about this earlier but I think I now understand the answer. To wit, I believe the main difference between anonymous record types and named record types is:
Now, my understanding is that named record types can optionally be tagged as 'struct' (i.e. values living on the stack), otherwise their values live in the heap. What about anonymous record types--since they are basically tuples, do they automatically live on the stack? |
@dsyme What about using the existing tuple notation for anonymous records? Wouldn't be a better idea than introducing a new construction like the one you propose ( I mean for type A we can use:
and for type B, simply:
The advantage is that it will re-use existing syntax, the only addition is the The disadvantage is that you'll need to disambiguate in the specific case that you want to create a tuple with a value coming from an equality test, but that's also the case right now with the named parameters, usually what I do in such cases is to invert the left and the right side. |
This let r = (x = 4, y = "") is undistinguished from a tuple of two bools creation ( |
Why do we need module CSharpCompatAnonymousObjects =
let data1 = new {< X = 1 >}
let f1 (x : new {< X : int >}) = x.X |
@vasily-kirichenko I know, read the last paragraph ;) |
@dsyme My impression is that one of the distinguishing properties between tuples and records is that field ordering is significant in the former. In other words I would expect > (x: "foo", y: false) == (y : false, x:"foo")
(1,1): error CS0019: Operator '==' cannot be applied to operands of type '(string, bool)' and '(bool, string)' If that's the case, how would we naturally represent "Kind A" records using either Tuple or ValueTuple? I imagine there would be a normal form in which fields are sorted? Could this also entail interop corner-cases because this is encoding records on top of a different thing? |
Thanks @eiriktsarpalis - I will add a discussion about ordering to the RFC
That would certainly be the natural, correct and expected thing, though it's unfortunately not what C# does.
Yes. If we take note of the C# field metadata then ordering has to be significant for those types coming in from C# |
Yes, typo, thanks |
It's a good question. As noted it would be a breaking change. I think my biggest concern is that the process of nominalizing code is then considerably more intrusive - lots of The syntactic overlap with named arguments is also tricky. I'll certainly note it as an alternative |
You add |
@dsyme So would |
@dsyme It also seems that passing "Kind A" records to any reflection-based serializer would cause the value to be serialized like a tuple. I understand that "Kind B" exists to address these types of concerns, but it still seems that "Kind A" may be violating expectations in what seems like the primary use case for this feature. It also looks like this might be an avenue for serious bugs, incorrectly writing to a database because somebody forgot to put a |
@dsyme It begs the question whether "Kind A" records would be better off using a dynamically typed representation at runtime, in the sense of |
Any reason why the syntax needs to be different for Records and Anonymous Records i.e. could one not use I'm thinking of just trying to explain to a newcomer to the language that there's Also - the use of the |
They are separate types erased to the same representation. The compiler doesn't consider them compatible.
Yes, it's possible. I'll note it in the RFC as a tradeoff/risk, and we should consider what we can do (if anything) to ameliorate the problem.
Creating objects to hand off to reflection operations is indeed one use case - though it's not the only one. The feature is useful enough simply to avoid writing out record types for transient data within F#-to-F# code.
Yes, C# 7.0 tuples are very much exposed to this - I think it's even worse there to be honest because there is even more reliance in C# on .NET metadata, and not much of a tradition of erased information. I believe many C# people will try to go and mine the metadata, e.g. by looking at the calling method, cracking the IL etc. However I think they will be frustrated at how hard it is to do, and in most cases just give up. A lot of this depends on how you frame the purpose of the feature, and how much reflection programming you see F# programmers doing. It is also why I emphasize the importance of nominalization as a way to transition from "cheep and cheerful data" to data with strong .NET types and cross-assembly type names.
I will list that as an alternative. It's certainly something I've considered, however I think it's just too deeply flawed - it neither gives performance, nor interop, nor reflection metadata. We can't leave such a huge performance hole lying around F#. |
Yes, it's just too ambiguous. I will note that.
Yes, whether it is can be explained to newcomers is crucial. I think the learning path would place these after the existing nominal record and union types.
I played around with pretty much every alternative I could think of - I'll list some in the RFC - by all means suggest others. It's just hard to find something that says "this thing has strong .NET metadata". Two data points:
|
what about let x = type {| i = 1 |} ? |
what about @vasily-kirichenko Yeah, I know. It's one of a bunch of things that could be considered to imply "this value has runtime type information", but each of which seems worse in other ways (in this case, to me "type" implies "what comes after this is in the syntax of types").
Just to mention that there is actually no way to do this. If you look at the compiled IL code for
you will see that all mention of |
It does seem to me that this type of usage would rarely escape the confines of a single assembly, and perhaps it shouldn't either.
I must say I'm biased towards serialization coming to this discussion, in which none of these points are a real issue. There is real potential in this feature when it comes to cheaply generating responses in, say, asp.net endpoint definitions (one of these rare points where you wish you'd rather have dynamic types). I am concerned however that the existence of two kinds of anon records whose vast differences in the underlying implementation are not highlighted in their F# syntax will be source of great confusion. All this makes me wonder why "Kind A" is necessary. Even though I find labeled tuples a fundamentally misguided feature, perhaps it might be worth supporting that instead in the interest of C# interop. |
I just found out there is an attribute in methods exposing labeled tuples: (string first, string middle, string last) Foo()
{
return (middle: "george", first: "eirik", last: "tsarpalis");
} compiles to [return: TupleElementNames(new string[]
{
"first",
"middle",
"last"
})]
private ValueTuple<string, string, string> Foo()
{
return new ValueTuple<string, string, string>("george", "eirik", "tsarpalis");
} But yeah, nothing for values themselves. |
It's fair to question the inclusion of both Kind A and Kind B anonymous types. There are, after all, reasons why both of these haven't been included in F# before. That said, I think we don't need to emphasize the differences between these more than is done in the proposal. The differences are crucial w.r.t. reflection but are unimportant for basic use cases where you are simply avoiding writing an explicit record type. As a general point, the norm in F# is to emphasize similarity, not difference. For example, we use "." for many similar things - even though accessing a field is vastly different to calling an interface method. We use To take one example, consider using an anonymous record type for CloudProcessInfo (let's assume for the purposes of this discussion that this data doesn't get serialized, or that we don't care of the exact form of the serialization as long as its consistent across a single execution of a cluster). In this case, either Part A or or Part B types would suffice. There's no difference. There are many such cases in F# programming, particularly cases where record types have only one point of construction. So the differences are highlighted by the use of
It's a good question. Interop with C# is not the primary consideration. It really comes down very much to this: Is the cross-assembly utility of Kind A types of sufficient worth to warrant their inclusion? Specifically, how painful is it if you can't create new instances of anonymous types mentioned in other assemblies, or even write the types in signatures in other assemblies (rather than relying on inference to propagate the types). |
Right, that's what I meant. So nothing for values passed to your reflection API, for example. |
They would still be fully statically typed, since that is up the the F# compiler front end which Fable uses
My impression is that there are semantic differences already, especially around reflection |
what semantics!? in fable everything becomes Pojo anyway. we unbox like crazy in the libs anyway |
In Fable at runtime all bets are off anyway - lots of stuff might behave differently. Even option types behave differently with null etc. to aid interop. |
Yes, I guess all I was trying to say is that such a decision should be made explicitly rather than by accident. |
Well that is quite a hard statement. Maybe we shouldn't call it F# if that is the case (which is the hard response to that I guess) |
@matthid it works really well ;-)
Am 06.10.2017 17:33 schrieb "Matthias Dittrich" <notifications@github.com>:
… In Fable at runtime all bets are off anyway
Well that is quite a hard statement. Maybe we shouldn't call it F# if that
is the case (which is the hard response to that I guess)
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#170 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AADgNKHyq9_rC-lmxZdoEVk5G5QKqoDGks5spkhTgaJpZM4MXR94>
.
|
@matthid by the same token, units of measure are a trick since they are erased away at runtime, too (as are probably 95% of type providers out there). They work great for me. In fact, one of the best things about UoM is that they can be added in retrospectively with very little concern of breaking changes at runtime e.g. performance degredation. |
I've been following this feature and I think it'll be very beneficial for Fable. If implemented, most chances is anonymous F# records would be translated by Fable to Plain JS objects, which is already possible using the Pojo attribute though you still need the nominal type. This means the type won't be a proper instance and won't have reflection metadata attached, but this is probably not very serious because
But... (and here comes the but) unfortunately I don't think anonymous records will be the panacea for JS interaction in Fable. Most of the times we have to create dynamic JS objects to pass a set of options to a JS library. TypeScript manages to give a statically-typed experience for this thanks to optional properties and structural typing, so the compiler will check if an object conforms to the expected options, even if it doesn't explicit all members. In Fable, we use the option type to represent this, but using a The only way I can think of at the moment to make anonymous records work well for this use case would be to make them compatible with an interface like the following: // Current Fable syntax to represent optional members
type JSOptions =
abstract foo: string option with get, set
abstract bar: int option with get, set
type MyJSLib =
abstract doSomething: opts: JSOptions -> unit
myJsLib.doSomething({| foo = Some "x" |}) It's been proposed to use static member constraints for this, but it's very cumbersome with current syntax and I think it cannot be applied to interface members. Currently Fable has two tricks to conform with an interface like let opts = createEmpty<JSOptions>
opts.foo <- Some "x"
myJsLib.doSomething(opts)
// JS
// var opts = {}
// opts.foo <- "x"
// myJsLib.doSomething(opts)
// This has also been added recently, it will compile directly to a POJO when possible
myJsLib.doSomething(jsOptions (fun o -> o.foo <- Some "x") |
@alfonsogarciacaro So if anonymous record types included optional properties would this make them more realistically usable for POJO data? e.g.
Note however that anonymous records are still going to be more rigid than one might expect from typescript etc. - you can't pass an object with more fields where one with few fields is required, or one which has non-optional fields to one where optional fields are specified. |
@dsyme Yes, that should help a lot. The most common scenario where you want to pass a POJO to a JS library is for option configuration, and using Records to type that is too cumbersome because it forces the user to instantiate all the fields at once. Right now we're using interfaces to translate Typescript definition. It'd be great if anonymous records could also be used that way: type IConfig =
abstract id: string
abstract x: int option
abstract y: int option
let myApi (myJsonConfig: IConfig) = ...
myApi {| id="1" |} // passes {| id="1"; x=None; y=None |}
myApi {| id="1"; x = 1 |} // passes {| id="1"; x=Some 1; y=None |}
myApi {| id="1"; y = 2 |} // passes {| id="1"; x=None; y=Some 2 |} ...but that's some sort of structural typing (I think there's already a lang suggestion for that) and I guess it's out of scope :) |
I'm thinking of cases where this could be useful. For example, in Fable.React bindings we use the let inline domEl (tag: string) (props: IHTMLProp list) (children: ReactElement list): ReactElement = ...
let inline voidEl (tag: string) (props: IHTMLProp list) : ReactElement = ...
let inline svgEl (tag: string) (props: IProp list) (children: ReactElement list): ReactElement = ...
let inline fragment (props: IFragmentProp list) (children: ReactElement list): ReactElement = ...
|
Im too lazy to analyze the spec again, anonymous records can they be used in a function definition, or is this part of the not supported row polymorphism part? |
A bit late to the party here, but after thinking about this some more, I think 👎 on the principle that these can cross assembly boundaries. I think that the primary utilities of this feature are:
And neither of these really implies that it needs to persist across assembly boundaries. True, you'd need to allocate again to get something to a consumer of the data you gathered, but I think it's better to be more restrictive as a principle now, and if there is a lot of feedback that they ought to cross assembly boundaries, we can do that in a change to the feature. If we go with the assembly boundary principle as-is and then learn that the behavior is confusing and/or abused too easily, we won't be able to go back and will then be forced to try and document "how not to use anonymous records", or something to that extent. |
I don't mind withdrawing from "cross assembly" as long as it makes it easy to implement them. I really want to see anonymous records implemented, at least a basic implementation. |
Sounds a bit like crossing assembly boundaries gives you a sort of row polymorphism? That can perhaps make it quite complicated to implement since then you'd need to mix different type systems (correct me if I'm wrong @7sharp9 )? |
@gusty They're actually already implemented (and quite complete) in the VF# repo. But what's not clear is: (a) Good understanding of what they would be used for beyond what is mentioned in my comment |
I think they are mainly useful to store intermediate values when doing data transformation. Of course, there are other use cases that are very interesting, but I think that's the most important one and the reason to have them available in the language. |
One major issue is that if they can't cross assembly boundaries, the F# programmer who uses them in a rapid-prototyping mode will quickly find they need to litter their code with I think that issue alone would mean that the feature tips over from being "worth it" to "not worth it" in many situations. And I have a very strong desire that the routine RAD use of core language features doesn't require the artificial introduction of So in balance my expectation is to keep things as is in the RFC. |
Hmmm, I think I understand. In that case, crossing assembly boundaries is fine. I'm just not that enthused at the concept of anonymous type being used as a publicly consumable contract. Though I suppose that can be documented here |
@cartermp out of curiosity–why? Anonymous types (aka structural types) are used to good effect in TypeScript. F# could get that same rapid development benefit. |
Further motivation: dotnet/machinelearning#1085 This change to ML.NET also offers a good set of scenarios to validate the feature against. |
This could be a useful feature to write signatures when declaring external functions in Fable: let foo(x: {| bar: string |}): {| baz: int |} = importMember "./util.js" My main concern is the structural equality/comparison semantics, because the objects coming from JS won't support it. How is it with C# interaction? Will an F# method whose signature includes anonymous records accept anonymous classes from C#? Apparently, C# anonymous classes have some kind of structural equality but I'm not sure if they support comparison. |
Closing as this is now in. |
Discussion for https://github.com/fsharp/fslang-design/blob/master/FSharp-4.6/FS-1030-anonymous-records.md
The text was updated successfully, but these errors were encountered: