Skip to content
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

Closed
dsyme opened this issue Mar 8, 2017 · 116 comments
Closed

[RFC FS-1030] Discussion: Anonymous record types #170

dsyme opened this issue Mar 8, 2017 · 116 comments

Comments

@dsyme
Copy link
Contributor

dsyme commented Mar 8, 2017

Discussion for https://github.com/fsharp/fslang-design/blob/master/FSharp-4.6/FS-1030-anonymous-records.md

@smoothdeveloper
Copy link
Contributor

I'd like to understand better the motives around two aspects of the RFC:

  • interop with annotated C# struct tuples
  • interop with C# anonymous types

What are the drivers for each?

For the first one, what I find compeling:

  • enables struct backing (but do we want that optional?)
  • IIUC, it would give a syntax to support the metadata C# emits for named struct tuples (although that would need extra work in terms of type check / error message to be on par with C#)

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:

  • the same as current C# anonymous types
  • pretty printing
  • possibility to annotate with struct () like for tuples (or equivalent)
  • record type semantics to construct new values (I really liked the possibility to add or drop fields that you hinted in discussion, this would give a nice advantage for data transformation scenarios compared to anonymous types as we know them in C#)

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 struct () construct to not become confusing.

I'd like to know if type alias will be supported as well.

@dsyme
Copy link
Contributor Author

dsyme commented Mar 9, 2017

if code returns them or take them as input ...it has to rely on reflection.

Correct, the .NET code we need to interoperate with uses reflection.

I'd like to know if type alias will be supported as well.

yes, type X = {| A : int |} would be supported

@dsyme
Copy link
Contributor Author

dsyme commented Mar 10, 2017

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

@sgoguen
Copy link

sgoguen commented Mar 10, 2017

Use Cases for Anonymous Classes

As 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();

}

@sgoguen
Copy link

sgoguen commented Mar 10, 2017

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.

@isaacabraham
Copy link
Contributor

@dsyme should this sub-section title actually read "design-principle-no-structural-subtyping?

@yawaramin
Copy link

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:

  • Anonymous record types are compiled down to tuples (this means they can't have user-defined static or instance members)

  • Named record types are compiled down to classes (this means they can have user-defined static or instance members)

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?

@gusty
Copy link
Contributor

gusty commented Mar 22, 2017

@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 (let r = {|x = 4 ; y = "" |})

I mean for type A we can use:

let r = struct (x = 4, y = "")

and for type B, simply:

let r = (x = 4, y = "")

The advantage is that it will re-use existing syntax, the only addition is the x = part, but this is also been used already for named parameters.

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.

@vasily-kirichenko
Copy link

This

let r = (x = 4, y = "")

is undistinguished from a tuple of two bools creation (bool * bool).

@Lenne231
Copy link

Why do we need new when using {< ... >} for kind B values? Is this a typo?

module CSharpCompatAnonymousObjects = 
    
    let data1 = new {< X = 1 >}

    let f1 (x : new {< X : int >}) =  x.X

@gusty
Copy link
Contributor

gusty commented Mar 22, 2017

@vasily-kirichenko I know, read the last paragraph ;)

@eiriktsarpalis
Copy link

eiriktsarpalis commented Mar 22, 2017

@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 |} to be of the same type as {| y = false ; x = "foo" |}, which is not possible using C# 7 tuples:

> (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?

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

Thanks @eiriktsarpalis - I will add a discussion about ordering to the RFC

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?

That would certainly be the natural, correct and expected thing, though it's unfortunately not what C# does.

Could this also entail interop corner-cases because this is encoding records on top of a different thing?

Yes. If we take note of the C# field metadata then ordering has to be significant for those types coming in from C#

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

Why do we need new when using {< ... >} for kind B values? Is this a typo?

Yes, typo, thanks

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

@gusty

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 (let r = {|x = 4 ; y = "" |})

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 , to turn into ; and ( to turn into {. (Of course one could argue that that's a problem with { ... } record syntax itself)

The syntactic overlap with named arguments is also tricky.

I'll certainly note it as an alternative

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

@yawaramin

What about anonymous record types--since they are basically tuples, do they automatically live on the stack?

You add struct to make them be structs (i.e. "live on the stack") for locals and parameters)

@eiriktsarpalis
Copy link

@dsyme So would {| X : int |} and {| Y : int |} just be aliases of Tuple<int> or would the compiler require unsafe conversion?

@eiriktsarpalis
Copy link

@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 new keyword before the record declaration. I guess this also affects C# 7 tuples, so wondering if there is a way for .NET libraries in general to mine this type of metadata.

@eiriktsarpalis
Copy link

@dsyme It begs the question whether "Kind A" records would be better off using a dynamically typed representation at runtime, in the sense of Map<string, obj>.

@isaacabraham
Copy link
Contributor

Any reason why the syntax needs to be different for Records and Anonymous Records i.e. could one not use { x = "15"} instead of {| x = "15" |} ? Or is this too ambiguous in terms of the compiler having to infer which of the two that you want?

I'm thinking of just trying to explain to a newcomer to the language that there's { } syntax, {| |} syntax and also new {| |} syntax.

Also - the use of the new keyword seems kind of arbitrary to me. Why should the new keyword signify a different runtime behaviour?

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

@dsyme So would {| X : int |} and {| Y : int |} just be aliases of Tuple or would the compiler require unsafe conversion?

They are separate types erased to the same representation. The compiler doesn't consider them compatible.

@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 new keyword before the record declaration.

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.

in what seems like the primary use case for this feature

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.

I guess this also affects C# 7 tuples, so wondering if there is a way for .NET libraries in general to mine this type of metadata.

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.

@dsyme It begs the question whether "Kind A" records would be better off using a dynamically typed representation at runtime, in the sense of Map<string, obj>.

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#.

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

@isaacabraham

Any reason why the syntax needs to be different for Records and Anonymous Records i.e. could one not use { x = "15"} instead of {| x = "15" |} ? Or is this too ambiguous in terms of the compiler having to infer which of the two that you want?

Yes, it's just too ambiguous. I will note that.

I'm thinking of just trying to explain to a newcomer to the language that there's { } syntax, {| |} syntax and also new {| |} syntax.

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.

Also - the use of the new keyword seems kind of arbitrary to me. Why should the new keyword signify a different runtime behaviour?

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:

  • I did trial the design with a relatively new F# programmer (not coming from C#). He was happy with "new" - the feedback was "yeah, I get it, new is a C# thing and this adds the .NET metadata C# would expect". So it doesn't come across as entirely arbitrary
  • There is the advantage that C# uses new { ... } for C# 3.0 anonymous objects. When converting LINQ queries or other code from C# the corresponding F# code will also naturally use new {| ... |}.

@vasily-kirichenko
Copy link

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".

what about

let x = type {| i = 1 |}

?

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

what about let x = type {| i = 1 |}?

@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").

@eiriktsarpalis

wondering if there is a way for .NET libraries in general to mine this type of metadata.

Just to mention that there is actually no way to do this. If you look at the compiled IL code for

var z = (c: 1, d : 2);

you will see that all mention of c and d has disappeared. I don't mind erasure in F# - though I actually find it a bit odd in the context of the overall C# design ethic and goals. But that's how it is.

@eiriktsarpalis
Copy link

eiriktsarpalis commented Mar 22, 2017

@dsyme

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.

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 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#.

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.

@eiriktsarpalis
Copy link

eiriktsarpalis commented Mar 22, 2017

@dsyme

Just to mention that there is actually no way to do this.

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.

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

@eiriktsarpalis

... differences are not highlighted in their F# syntax...

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 type X = ... and let ... = ... for many different things. In each cases there is an underlying similarity we want to emphasize. There are limits to that approach, but it's a basic rule in the F# playbook.

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 new. Are they are highlighted enough? Well, other differences such as struct are highlighted by a single keyword. Given that, I'm comfortable that we're tuned approximately right on the level of syntax used to highlight the difference.

All this makes me wonder why "Kind A" is necessary. ..

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).

@dsyme
Copy link
Contributor Author

dsyme commented Mar 22, 2017

Just to mention that there is actually no way to do this.

I just found out there is an attribute in methods exposing labeled tuples... But yeah, nothing for values themselves.

Right, that's what I meant. So nothing for values passed to your reflection API, for example.

@dsyme
Copy link
Contributor Author

dsyme commented Oct 6, 2017

I mean anonymous types are stillfully statically typed

They would still be fully statically typed, since that is up the the F# compiler front end which Fable uses

Yes I mean i honestly don't know if it is a good idea to have different semantics between .net and fable.

My impression is that there are semantic differences already, especially around reflection

@forki
Copy link
Member

forki commented Oct 6, 2017

what semantics!? in fable everything becomes Pojo anyway. we unbox like crazy in the libs anyway

@isaacabraham
Copy link
Contributor

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.

@matthid
Copy link
Contributor

matthid commented Oct 6, 2017

My impression is that there are semantic differences already, especially around reflection

Yes, I guess all I was trying to say is that such a decision should be made explicitly rather than by accident.

@matthid
Copy link
Contributor

matthid commented Oct 6, 2017

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)

@forki
Copy link
Member

forki commented Oct 6, 2017 via email

@isaacabraham
Copy link
Contributor

@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.

@alfonsogarciacaro
Copy link

alfonsogarciacaro commented Oct 6, 2017

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 sprintf "%A" and JSON serialization will still work the same as for nominal records. It will also be very helpful to interact with JS libraries (like React) that only accepts plain JS objects and not typed instances. And I'm also very happy to see that anonymous records won't allow prototype or static members, as they're forbidden in Pojo records too which causes confusion sometimes.

About F# semantics in code compiled by Fable, we try to keep a balance between keeping most of F# semantics so users don't have surprises at runtime while also outputting JS as standard as possible and with very low overhead.

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 Pojo record still forces the user to make all members explicit, which in most cases is not feasible.

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 JSOptions (used in many JS bindings), both of them forces the interface to mark members as mutable:

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")

@dsyme
Copy link
Contributor Author

dsyme commented Mar 14, 2018

@alfonsogarciacaro So if anonymous record types included optional properties would this make them more realistically usable for POJO data? e.g.

let myApi (myJsonConfig = {|  id: string; ?x : int; ?y: int  |}) = ...

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 |}

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.

@alfonsogarciacaro
Copy link

@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 :)

@alfonsogarciacaro
Copy link

I'm thinking of cases where this could be useful. For example, in Fable.React bindings we use the keyValueList trick that converts a list of union cases into a POJO. We could use anonymous records instead so the code looks closer to the React API and we can also start options with lower case. However, we have several methods accepting this object so listing all properties in every signature wouldn't be practicable:

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 = ...

See https://github.com/fable-compiler/fable-react/blob/ff7b5ff0033dac63f73a8e0862c3b508addee85d/src/Fable.React/Fable.Helpers.React.fs#L917-L952

Please note this is just an idea and maybe replacing the keyValueList with anonymous records is not entirely desirable. Besides breaking existing code, this would make it more difficult to compose configuration which many users currently do by appending lists.

@7sharp9
Copy link
Member

7sharp9 commented Jun 19, 2018

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?

@cartermp
Copy link
Member

cartermp commented Jun 28, 2018

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:

  • Avoiding "domain modeling for your JSON payload" in .NET and Fable
  • Ad-hoc grouping of data as you need it for a computation

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.

@gusty
Copy link
Contributor

gusty commented Jun 28, 2018

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.

@wallymathieu
Copy link

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 )?

@cartermp
Copy link
Member

@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
(b) If cross-assembly is a good idea (I don't think it is, and right now I wouldn't feel comfortable releasing the feature with that right now)
(c) If people fully understand what they are good for and what they are not good for
(d) Vectors for abuse that we'll regret later, especially after not letting them "bake" for a while

@gusty
Copy link
Contributor

gusty commented Jun 28, 2018

I think they are mainly useful to store intermediate values when doing data transformation.
Right now we have to either use tuples, even if we're dealing with 10 fields, or declare records for all intermediate representations.

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.

@dsyme
Copy link
Contributor Author

dsyme commented Jul 4, 2018

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.

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 private and internal and so on, even though they don't have any potential or imagined consumer of their code, otherwise the compiler will keep giving warnings "this type may escape its assembly scope".

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 private and internal annotations.

So in balance my expectation is to keep things as is in the RFC.

@cartermp
Copy link
Member

cartermp commented Jul 5, 2018

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

@yawaramin
Copy link

@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.

@cartermp
Copy link
Member

cartermp commented Oct 3, 2018

Further motivation: dotnet/machinelearning#1085

This change to ML.NET also offers a good set of scenarios to validate the feature against.

@alfonsogarciacaro
Copy link

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.

@cartermp cartermp added this to the F# 4.6 milestone Nov 13, 2018
@cartermp
Copy link
Member

cartermp commented Mar 1, 2019

Closing as this is now in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests