fsharp / fslang-design

RFCs and docs related to the F# language design process, see https://github.com/fsharp/fslang-suggestions to submit ideas
515 stars 144 forks source link

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

Closed dsyme closed 5 years ago

dsyme commented 7 years ago

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

smoothdeveloper commented 7 years ago

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

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

dsyme commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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

yawaramin commented 7 years ago

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?

gusty commented 7 years ago

@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 commented 7 years ago

This

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

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

Lenne231 commented 7 years ago

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 commented 7 years ago

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

eiriktsarpalis commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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

Yes, typo, thanks

dsyme commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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

eiriktsarpalis commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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

vasily-kirichenko commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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.

eiriktsarpalis commented 7 years ago

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.

I think that the main reason for concern is libraries/frameworks that claim to support any type e.g. createHttpResponse: 'T -> HttpResponseMessage, where writing

createHttpResponse({| X = 2 |})

or

createHttpResponse(new {| X = 2 |})

has no evident difference as far as syntax and type checking are concerned, but at the same time greatly alters runtime semantics. I can easily picture myself making this mistake. In homogeneous clusters like mbrace, the pickling format doesn't really matter: types will get deserialized as they got serialized. However, the story becomes quite different in heterogeneous systems, e.g. microservices reading and writing json. The mistake above will break the api contract of the microservice.

This is too dramatic a change for something as innocuous as the new keyword. struct on the other hand introduces a well-understood modality and all the different flavours of let do not fundamentally change the type of the value they're binding on.

Is the cross-assembly utility of Kind A types of sufficient worth to warrant their inclusion?

I'm starting to think that it isn't. It does seem to open up new vistas of abuse where every aspect of a public API is exposed as an anonymous record (read: tuple). If anything, a public API requires careful curation and supplying good names for exposed types is part of that process.

eiriktsarpalis commented 7 years ago

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.

Another reason why supporting labeled tuples might be warranted is also because of the expected advent of C# libraries misguidedly exposing them as part of their public API. In C# they'll have friendly names, in F# it's going to have to be Item1 and Item2.

isaacabraham commented 7 years ago

A couple of more thoughts / ideas.

@dsyme Regarding ambiguity - at the risk as exposing my lack of awareness of the issues with writing a compiler, what's wrong with rules as follows: -

type Foo = { Name : string; Age : int } // normal record
let foo = { Name = "Isaac"; Age = 21 } // anonymous record

i.e. it appears (to me!) that it should be clear to see when the developer wants an anonymous record as opposed to a conventional record with a specific name. What sort of cases are there where there could conceivably be a clash between declaration of both? Or is it more about getting the developer to be clear in terms of intent and readability to others when using {| |}?

Another question - how will you reference such an anonymous type in another function e.g.

let x = { Name = "Isaac"; Age = 21 }
let foo value = value.Name

If there was a need to specific what type value was e.g. to aid compiler type inference (particularly when you have two records with similar shapes and the compiler picks the wrong one) - would it be possible?

Perhaps syntax such as this: -

let x = { MyRecord.Name = "Isaac"; Age = 21 }
let foo (value:MyRecord) = value.Name

whereby the type name is declared at the same time as constructing the object. This also uses existing F# record syntax so would not be unfamiliar to the F# developer.

vasily-kirichenko commented 7 years ago

@isaacabraham what about

type Foo = { Name : string; Age : int }
let foo = { Nane = "Isaac"; Age = 21 } // (note the typo) no compilation error, but the type is not what you wanted. 

It'd be very annoying to fix such bugs.

eiriktsarpalis commented 7 years ago

@isaacabraham I think that this syntax would break backward compatibility wrt type inference. In current versions of F# foo would be unambiguously typed as Foo, whereas introducing anonymous records using the same syntax would render it ambiguous.

It does seem to me though that "Kind B" anonymous records could be safely introduced using a new { Name = "Isaac"; Age = 21 } syntax.

isaacabraham commented 7 years ago

@eiriktsarpalis but don't you already have that issue if you had another record that had the same fields? @vasily-kirichenko Yes, I thought about that too. I remember thinking that there might be some workaround for that - perhaps with compiler warnings / hints - but yes, implicitly making typos and not realising might be a problem.

eiriktsarpalis commented 7 years ago

@isaacabraham well the problem is that for every nominative record type, there will be a matching anonymous type. So that annoying problem we currently have with conflicting field names will now be a universal issue.

yawaramin commented 7 years ago

@isaacabraham there is currently a way to disambiguate what type of record you want--put the record type in a module and use the module name for namespacing:

type person = { id : int; name : string } module Person = type t = { id : int; name : string; age : int }

let getId { id = i } = i let getPersonId { Person.id = i } = i;;

@dsyme will anonymous record fields be disambiguated by their enclosing modules as well?

dsyme commented 7 years ago

@yawaramin

@dsyme will anonymous record fields be disambiguated by their enclosing modules as well?

No

dsyme commented 7 years ago

@eiriktsarpalis

It does seem to me though that "Kind B" anonymous records could be safely introduced using a new { Name = "Isaac"; Age = 21 } syntax.

This is a possibility. However I'm quite keen on a syntax that doesn't need double parentheses in the common case, e.g. someFunction (new { Name = "Isaac"; Age = 21 }) v. someFunction {| Name = "Isaac"; Age = 21 |} or ``someFunction (name = "Isaac", age = 21 )

That is also relevant to the question of whether Kind A are supported at all, and if they are whether thy are the default.

dsyme commented 7 years ago

@eiriktsarpalis What would you think of

  1. Kind B the default, so {| X = 1 |} is Kind B.
  2. Allowing Kind A by some other means, like a keyword anon {| X=1 |} or named tuple syntax (x = 1, y = 2) and struct (x = 1, y = 2).
eiriktsarpalis commented 7 years ago

@dsyme I'd go for the named tuple syntax, for two reasons:

smoothdeveloper commented 7 years ago

I had the anon keyword in mind but for use with Kind B (as they match with C# anonymous types) and also was thinking that matching C# syntax for tuples would be helpful for people doing back & forth between C#/F#.

The fact that {| stands out more than plain records is good.

I understand concern about ambiguity between method signature and tuple literals, in C# they were made to match method parameter dfinition when used as return type; it would be nice if that feature could afford us something better than struct (,,,,,,,,,,) syntax.

Are we definitive that we want/need both A & B representations and value/reference for both?

dsyme commented 7 years ago

Are we definitive that we want...

Nothing is definite at this stage, it's just discussion :)

isaacabraham commented 7 years ago

The named tuple syntax (which is more like C#) is nice in that it matches with the underlying runtime information - and there's no confusion between Kind A and Kind B as you have distinct syntax etc. as well. But then you completely lose the ability to have the "easy migration" from Kind A to either Kind B or "full" records.

I also liked the idea of thinking about named tuples as anonymous records - plus what syntax would you use to reference fields on a named-fields tuple?

0x53A commented 7 years ago

Regarding named tuple syntax, consider that the following is currently a two-tuple of type bool:

Microsoft (R) F# Interactive version 4.1
Copyright (c) Microsoft Corporation. All Rights Reserved.

For help type #help;;

> let a = 1
let b = 2
(a=2, b=2);;
val a : int = 1
val b : int = 2
val it : bool * bool = (false, true)
dsyme commented 7 years ago

what syntax would you use to reference fields on a named-fields tuple?

tup.X

dsyme commented 7 years ago

Regarding named tuple syntax, consider that the following is currently a two-tuple of type bool:

Yes, it could be that, if we go in that direction, we would drop the major focus on Kind A altogether and only allow named ones on C# struct tuples struct (x=1,y=1) where the syntax is still available

If so we would do that as a whole separate PR.

0x53A commented 7 years ago

@dsyme But isn't that the same?

let a = 1
let b = 2
struct (a=2, b=2);;
val a : int = 1
val b : int = 2
val it : struct (bool * bool) = (false, true)

I actually like using a slightly modified tuple syntax, would it be possible to use : instead of =?

Edit: @eiriktsarpalis right, forgot that ...

eiriktsarpalis commented 7 years ago

@0x53A : has a strong association with type annotations.

gusty commented 7 years ago

@0x53A As I mentioned earlier, confusing the field-name-and-value with a bool comparison is already in the language when you call a method with named parameter, for example we have already situations like this:

type X =
    static member M (a, b) = (a, b)
let a = 0
let b = ""
let x = X.M(b = 1, a ="")       // val x : string * int = ("", 1)
let y = X.M(a = 1, b ="")       // val y : int * string = (1, "")
let z = X.M((a = 1), (b = ""))  // val z : bool * bool = (false, true)

But using an additional parenthesis to disambiguate is not a bad compromise.

It's true however that given that this notation was not available for tuples, for existing code with boolean comparisons that doesn't have yet the additional parenthesis, it would be a breaking change. But it would be aligned now with the method calling syntax.

@dsyme I was also going to suggest something like: new { Name = "Isaac"; Age = 21 } as @eiriktsarpalis already did for Kind B.

It's true that it introduces the need for an additional parenthesis, but we have already that situation in most other proposals for Kind B, I guess the forward pipe will have to come to help, as usual:

new { Name = "Isaac"; Age = 21 } |> someFunction
isaacabraham commented 7 years ago

wouldn't treating the following as an int * int named tuple be a breaking change then?

let a = 1
let b = 1
let x = (a = 1; b = 1) // F#4.1 - bool * bool; F#4.vNext = int * int.