-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
Reconsider recommended casing/naming for tuple members in corefx. #27939
Comments
Tagging @stephentoub @agocke @sharwell @bartonjs |
Also curious what @MadsTorgersen @DustinCampbell have thought on this topic. You two have demo'd this stuff a bunch at conferences. Do either of you have a preference on how these things are most naturally thought about and named? Thanks! |
I'm not sure why that's unfortunate. It's exactly the case today when storing the value of a public field/property on a type returned from an API into a local: the former is PascalCased, the latter camelCased.
I'm not sure who "we" is here, but we've never pushed on a position here for public .NET APIs. Folks can do what they want in the internals of their code/repos, but when we start talking about public APIs, there needs to be crisp, clear guidance, which is what we tried to do as part of this discussion.
There are three possible outcomes here from a framework design guidelines perspective:
I really, really, really dislike (3); I literally would rather we not ship the API at all (e.g. the new overload of Enumerable.Zip) than do that. From an API perspective, these are structs with public named property/fields, and the only reason to use a ValueTuple instead of a custom struct is to save on some effort in defining another one (and maybe there's an argument about binary size of the same generic instantiation could be reused). From that perspective, camelCasing completely breaks with every other public .NET API, where types returned from methods expose their data with PascalCasing, period. I don't have strong preference between (1) and (2), though I generally lean towards (1). |
cc: @terrajobst |
ps @CyrusNajmabadi, thank you for opening the new issue. :) |
I tried to explain that bit, but probably did a poor job. Sorry!. In this case, because the idea (to me at least) is that really is strongly just a bundle of these values. I've definitely acknowledged that these can just be thought of as nothing more than a simpler way to right a named-struct. But i think that's losing out somewhat on what tuples are really trying to get across, which is that they really can just 'fade out' and you aren't thinking of them at that level. i.e. without question, under the covers this is a real struct. Not question about it. But tuples are intended for lots of code to help you avoid even needing to think about that. Instead of thinking about it as a struct with these public fields, it's more like "here's just these few pieces of data" and the tuple fades out. The 'fading out bit' was a pretty intentional part of the design, and why we def invested in this like deconstruction. It wasn't necessary, and it's def not even that bad to just use the actual tuple instance value. But there was a strong enough push on the belief that the tuple itself wasn't relevant, but what was inside was what mattered the most. In this view of the world, the conception of the world is not that there's this struct with fields that you're using. It's just that you're flowing individual values along. And, just as parameters flow in as lower-cased-named data into a method, a tuple would allow you to flow lower-cased-named data out of the method. Another way of thinking about it: if we allowed you to name a return value of a method, would we intuit that that name should match how we do parameters (since it's just named data flowing in/out of the method)? Or would we name the data that flows out with a capitalized name? I honestly don't know :) But my gut tells me we would cameCase. And since the tuple is just a bundle of those named-return-data values, camel-casing makes the most sense there to me.
Sorry, i meant: during teh original designs of tuples and the general philosophy around them when we did things in the LDM. But i could be totally off base here and this might have just been my own perception on how I personally thought about tuples. Would def like @MadsTorgersen @DustinCampbell to weigh in. @jcouv and @gafter were also heavily involved IIRC. i'm curious their thoughts too :)
Absolutely. No question :) That's why i wanted to have this discussion. I'd love totally clear guidance here. And i'm 100% willing to accept that if corefx just cements the position of "they're pascal cased. discussion done" :) I'm just wanting to bring up the topic because i personally feel that this warrants at least a second look and some thought as to how we expect people to use these API, and what might feel more natural within those expectations. I personally have felt that camelCasing fits best here (and the adoption across the roslyn codebase itself has seemed to back that up), and i def think ti would make sense for corefx as well. If you guys agree, great. If you think they should be PascalCased, that works for me as well :) Thanks! |
Thanks. |
Note: since there are two seemingly valid (in my mind at least) ways to think about things, i'm not sure i can add anything else helpful. There's no right/wrong answer here, so it's probably better not that you've heard my perspective to maybe re-assess and make a decision if anything needs to change here. If you land on keeping the status-quo have no problem on that. This will hopefully prevent a ton of posts that are just going in circles :D If you need any more info/perspective from me, don't hesitate to ask. But i'll leave it in your hands now since i don't think i have much more to add. Thanks! |
The only thing that doesn't seem an option to me is
The point of convolution is to return a tuple. I originally had the tuple unnamed. I'm not sure why we needed to get into this debate at all. |
From a consumption standpoint, I don't know what "unnamed" means. The properties/fields would then be called Item1 and Item2, so they have names (and they're PascalCased). |
C# refers to a tuple without names in the syntax as an "unnamed" tuple. It has nothing to do with property names. |
Ok. But this is about how the API is consumed, and whether the thing that's returned has members:
I don't see how (1) is better than (2), and I don't like (3) :) |
If it's unnamed in the syntax we can't have this debate as the only thing you can have is (1). The point is to not talk about this. |
If I returned |
Yes, in that case we'd then be debating whether to instead return: public readonly struct ZipResult<TFirst, TSecond>
{
public ZipResult(TFirst first, TSecond second);
public TFirst First { get; }
public TSecond Second { get; }
} |
Ha OK then I'm out. I don't care. |
@terrajobst Can we continue the discussion here? Twitter just isn't a good medium for this sort of thing. Note the above posts that call out how naming things like fields/properties as being totally reasonable. However, i feel there's another side of the coin not being considered, which has a fair amount of support from several LDM members. I would like it if this idea could at least be considered since it certainly has some merit as well. Thanks! |
This is the crux of the issue. Part of the intuition we've had when designing and presenting this i that one doesn't need to think about this as "member access" at all. Note: to be clear, i'm not saying that thinking about is member-access is wrong, or innapropriate. I'm just pointing out there are alternate intuitions. In roslyn itself we've seen this intuition borne out, where lots of code works with tuples, but doesn't care about member accesses at all. In that regard, these aren't "members", they're just a collection of data. With that (afaict) equally valid intuition on how things work, these more naturally map to things like parameters, and are named naturally as such. |
Right. And there's one of the core contentious points. Do we expect people to think "i am consuming a Tuple. It has members that i'm accessing."? Or do we expect people to think "i'm consuming two pieces of data, 'the first piece, and the second piece'"? Both of these are equally valid perspectives given how this language feature has been created and how we allow it to be used. The fundamental question to me is: is one of these perspectives one we want to give higher preference to. On a solely personal level, i find the latter perspective much more intuitive and natural. We shaped tuples very particularly to feel a certain way and to match how we've done this elsewhere in the language. We've pushed a lot on the messaging that it's just a bundle of data. That you generally don't think or care about it as this container with members. The question here is: which perspective on things does CoreFx want to take? Note: i recognize this is a challenging topic as CoreFx is not C#. And it has to consider potentially many consumers and how they may think about these types. However, from C#'s perspective, tuples were meant to naturally fade-out, giving natural access to the actual data. And in that perspective of things, the names most naturally matching how they will be used, and having symmetry across consumption/production seems very nice. Thanks! |
Personally I think of tuple elements as a temporary grouping of values (like method parameters) rather than being independent API elements (like fields and properties). The creator and consumer of a tuple type (that has names) can use the tuple either with or without the names. By analogy with parameter names, where the names are used at the option of the consumer, a method that returns a tuple should return them with camelCased names to (optionally) help the consumer. That's the way I've been using them and it feels right. |
Could this position be reconsidered based on the information present about how several LDM members think about this, and how there are multiple valid ways we can expect people to think about tuples? In particular, i think this point is very important as part of the consideration:
CoreFx here is pushing a perspective which i believe is not necessarily the one that C# wanted when we introduced this feature. I would prefer the discussion happen again to make sure all parties are ok with that, or if it would be more appropriate to change things here. Thanks! |
When deciding on the guidelines for the BCL we take into account (sometimes with more success than others) CLR languages other than C#. NETCOBOL, for example, doesn't look like it will be able to make use of deconstruct (at least, at first glance I don't see it fitting in their language), but I can easily imagine their tooling consuming the The only place, if I understand correctly, that the
The decomposed syntax mainly falls out from ValueTuple having a Deconstruct method, and any casing on the IDE replacing As far as I know, the only real problem with this currently is that the suggestion/correction for "hey, you can deconstruct this |
The problem is the conceptual asymmetry. Even though the perspective is that the values are the same, there is a need to name them differently. This is not an issue if you don't htink of the values as being the first-class nature of the tuple, but it is if you do.
I don't believe that's true. I believe tuples can always be decomposed even without a Deconstruct method. And, even with the deconstruct method, the decomposition is there to operate on the data as it was naturally expressed by the producer.
I'ts not really an IDE-ism. It's how hte C# language views this symbol. The symbol is a "tuple, with these element names". There is a specific encoding/decoding C# uses (with ValueTuples, and fields, and attributes), but the intent is for that to only be known insofar that it's how to interoperate with other languages. The names are specifically intended to name the data. That that data is stored at the end of the day inside fields isn't something you generally need to know or care about. Fundamentally, C# created and encoded these to (as neal said) create a temporary grouping of values. When dealing with the values, the natural form is to just grab hte data and work with it directly (and, c# coudl have always gone with that being the only way to work with the tuple's data). But, frankly, that would likely be a little too onerous, so it's possible to both work with the tuples instance as well as just the data.
It depends on how you define 'real problem' :) As i mentioned in the first post, nothing at all breaks when using pascal-named tuple elements. You definitely can go this route. But, like neal said the other way just "feels right." So it depends on if you think it's a problem to ship a naming convention (that we'll likely not be able to ever change) that does not 'feel right' to people. IMO, that is a significant problem, and one that would be worth avoiding now so we don't end up with something we regret and which doesn't feel right to users down the line. Thanks! |
I'm with @CyrusNajmabadi and I view tuples as nothing more than a loose grouping of locals and/or parameters. The "container" exists only as a necessity in order to bridge the technical/legacy gaps in the language and runtime when passing those elements around. The tuple shouldn't be a cheap and easy replacement for a proper type with proper fields. Going with the |
I can understand this argument. To languages that don't understand tuples and see them as a container using field/property name guidelines make sense. But how many of those languages even understand tuples enough to support the element aliasing? Won't the majority of them just see |
I use tuples a lot. They are one of my favourite features of C# 7. In all of the code examples from the language team I can think of, they used camelCase for the element names of those tuples. This is the convention I've therefore followed too. As @gafter says, a tuple is a collection of values, much like locals or parameters. In fact, when the tuple syntax is used for a deconstruct, those elements are locals. Therefore using camelCase for the names does "feel right". Under the hood, those tuple elements are then just properties of a struct. But that's an implementation detail. The tuple syntax hides that implementation detail away from the developer. By insisting that, because they are properties, the naming should follow the naming convention of properties, you are making the abstraction "leaky" by surfacing those implementation details. Witth the API that @CyrusNajmabadi's mentions, public static IEnumerable<(TFirst First,TSecond Second)> Zip<TFirst, TSecond>(
this IEnumerable<TFirst> first, IEnumerable<TSecond> second) we are effectively doing |
I think tuple members should match the casing of the pseudo-tuple of method parameters. That is, camelCase. Most languages will still use ItemX names to access them and should ideally implement tuple deconstruction before or at the same time they implement tuple member names. |
I think there should be a fourth point here: to never use tuple names in public APIs. Specific names are useful in specific scenarios. But when it comes to a public API, I think we should not give opinionated names (by any reasoning) to elements, rather, we could just return |
What’s wrong with the idea of eschewing tuples in public API because they’re difficult to name consistently? The naming of API elements is important. Tuples are a great way to pass data around without ceremony, but public API design is all about selecting the appropriate ceremony. |
No, it couldn't. The argument is that tuples syntactically mirror parameter and argument lists, whereas constructor parameters and field declarations are unrelated. As @CyrusNajmabadi said, tuple expressions actually consist of an argument list in the syntax - it's the same syntax node and there is no difference between a tuple expression and an argument list of an invocation. |
The intensity of this discussion shows that tuples should not be in any public framework API at this point. Once they are in they will be in forever. Design mistakes cannot be fixed. |
Adding names later wouldn't be a breaking change. I think we could go with unnamed tuples and see how the client code would ever use these. I'd expect it to be deconstructed to locals most of the time, but who knows. |
Agreed. It seems like a good way forward is to just remove these.
This is the underlying representation. But it's not the language representation. And the names exist to support the language representation. Tuples were designed so that you could work with teh underlying representation (after all, you could be non-C#, or C# pre-tuples). However, the naming part was to allow C#/VB to rountrip these element names. The language representation isn't that htey're fields. They could honestly be anything. Instead, they're jut named elements that are semantically and syntactically bundled together to look like they're parameters. By naming things in this manner, it goes against the symmetry and intuition that the language features were designed against.
THe core problem here is one of perspective, and if you think the names map to the underlying representation (i.e. fields) or if you think the names map to the language projection. IMO, the names were specifically designed for exactly the latter purpose. So, while they're used to map fields in that regard, the names are "objectively** more consistent" in that they map them to parameter-like data. It's very intentional that we didn't design tuples to look like Instead, we intentionally designed them to project into the language as Thanks! -- ** Note: i personally don't think it's objective. I think it's rather subjective TBH :) There are two perspectives on what these thigns are, and the BCL straddles both worlds. It just that i think the name-attribute was meant to project C#'s perspective here, so it would be subjectively better to align to that rather than IL/metadata's perspective. |
I don't believe that's the entire purpose for their existence though. Mads and I had a conversation about this early on and we concluded that having names is a pre-req to use them in public APIs ever.
As a framework designer, it's a our responsibility to allow it to be used by a variety of languages and tools. In principle, there is nothing C# specific about tuples or names. Other languages, such as F# or even Cobol.NET could use the names as they are recorded in metadata. Quite frankly I think the conclusion on this thread is sadly that we can't agree on the bare minimum that would allow these types to be used in any public APIs. I also find myself agreeing with @stephentoub that the tone and way this discussion has evolved is rather frustrating. Since we have bigger problems we need to solve right now I think the conclusion here is: DO NOT use tuples in public APIs. That means their use and naming are private implementation details so anyone is free to make up their mind on how they want to name them. Personally, I feel this outcome is rather unfortunate because it's virtually guaranteed to create a mess, but hey. |
Is it a joke? |
I dislike camelCase, everywhere. |
camelCase is used extensively in .Net. Most importantly, it's used for locals/parameters. Which are the types of data that C#/VB tuples were designed to represent an aggregation of. C#/VB specifically picked syntax that mirrors these constructs intentionally as the intuition was to think about them as if they were the same. i.e. you could just 'pick up' your parameter list and make it into tuple and then pass that along (both into and out of methods). As such, the C# perspective on these guys is that they would naturally follow how parameters/locals are named. And those are widely written by nearly the entire ecosystem as camelCased. If .net switched to making parameters PascalCased, then that would follow for tuples as well. |
I'm disappointed that your conclusion is "DO NOT use tuples in public APIs". This is not good advice in my view. It's a great shame that the various Microsoft teams cannot agree a common strategy to naming conventions for tuple elements. You are right that it will create a mess. Many, myself included, will just ignore your conclusion and carry on using tuples in public APIs when a tuple is the right thing to use. But there will be no accepted convention that we can follow for their names, so both camelCase and PascalCase will be used by different people for their APIs. |
@DavidArno both naming choices have pretty big downsides. It's not necessarily a matter of just agreeing, making peace and going with it. This is not just a social problem. There is simply no viable alternative. I have faced the same naming conundrum in my own code. I also concluded that tuples should only be used in internal places (for some vague, intuitive notion of "internal"). |
What's wrong with using |
Well, named tuples are a language feature, so they would be specific to the language that defines them, no? AFAICT:
I can understand a "do not use names" or "do not use VT" rule for reasons to do with the BCL having to support multiple languages... but I don't see any language pushing for PascalCased(-only) tuple names. It appears to be only BCL developers, which puts CoreFX at odds with all it's major consumers... for ideological reasons not adopted by any language design team?? 😕 |
Looks like precedent has now been set that tuple element names should be pascal-cased when used as a return type: dotnet/corefx#35595 |
I like Pascal Case. Everywhere. |
I take it we shouldn't call you @tonyHenrique then =) |
Yes,, @bartonjs is working on guidelines for tuples, it basically is:
|
It's more "If you use tuples, use named tuples." (which happens to mean ValueTuple + attributes). Returning (unnamed) ValueTuple is a no-no. |
It’s extremely disappointing that the BCL team are in any way trying to offer guidelines on a language feature. All three guidelines are unhelpful and miss the point of tuples completely. Whether to use a tuple or custom struct/class is both subjective and dependent on many factors. A simple “prefer one over the other” guideline is a nonsense. If you use tuples, use the And as this thread shows, the BCL desire to |
There must be guidance for their usage in the BCL to keep the BCL APIs consistent. That is the BCL team's job. This a naming convention, the BCL already defines many of them (his is nothing new) to keep the BCL consistent. It should be noted that "using PascalCase runs against convention" is not a given - it may go against your convention but as this thread shows there isn't one. For example, we use Pascal internally. There needs to be a defined convention in the BCL, which is why this was a blocker to exposing APIs using them. Try to imagine for a moment if any other external API in the BCL didn't have a naming convention and where we'd be. |
@DavidArno it has always been part of the role of the .NET Libraries team (going back to the original Framework Design Guidelines) in collaboration with others such as the language designers and the community, to provide guidelines intended to help create better, more consistent.NET libraries. |
@DavidArno I can attest that this is the result of a joint discussion of representatives within the .NET team, including language/compiler and BCL. Like many such decisions, it is not easy, but I think it is ultimately helpful to have an overall agreement and consistent approach. So even though we didn't land on my initially preferred approach, I'm glad we landed on a decision and I'm satisfied with it. |
I agree with @jcouv . This is definitely not my preferred approach. However, my major desire was that there be a joint conversation with the relevant stakeholders on the matter. That appears to have been done. And, like with many decisions, not everyone would be satisfied. However, everyone got to have their voice heard and the decision was made understanding the ramifications. That's good enough for me. |
dotnet/corefx#26582 Added a new 'Zip' extension for IEnumerables with teh following signature:
The capitalization of the tuple members names (i.e.
First/Second
) was somewhat surprising to me as that wasn't the naming pattern that I thought we generally followed when creating and presenting tuples, and was not the naming pattern we've used in dotnet/roslyn itself.The intuition here was that tuples very commonly act as a shorthand way to bundle lightweight data together so they can easily be grouped and passed in and out of methods without hte heavyweight need to define an entire struct (and all the rest of the ceremony you would need there).
This naturally leads to two reasonable interpretations of things that could influence naming decisions:
PascalCased
).camelCased
).IMO, from my recollection of the design here in the LDM, we'd been pushing harder on the second position. We've put a lot of effort into making it feel very natural to just 'splat/deconstruct' tuples, pushing the positoin more that the tuple is less relevant, and it's just a bundle of variables you'll be working with locally in your methods. While it's true that that doesn't necessarily mean the tuple field names have to match the expected usage names, it feels unfortunate that these names would now have to differ in case. This goes for both consumption, where field names are being splatted into different local names, as well as construction. It's extremely natural for someone to just construct a tuple like so:
(name, age)
, but this would, for example, now create a tuple with names that don't actually match a method that returns something like(string Name, int Age)
. This lack of symmetry obviously is not a problem in terms of compilation (the language allows and converts here). However, it feels problematic to me because the code is now no longer working consistently with the same names that flow in, get worked on, and flow out.This is not a huge deal. But i think it warrants discussion as any decision made here will effectively impact all future usages of tuples across our APIs and our customers' APIs.
Thanks very much.
--
Note: additional data points. Both swift and go allow one to return multiple named values from a method/function. In both those cases, naming of those multiple-named values matches parameter naming.
The text was updated successfully, but these errors were encountered: