-
Notifications
You must be signed in to change notification settings - Fork 4.1k
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
Proposal: Provide generalized language features in lieu of tuples #12654
Comments
Some valid concerns raised, but I am not sure about custom attribute compiler magic.
This would certainly be more useful than e.g. reference counts http://stackoverflow.com/questions/17847927/how-to-hide-reference-counts-in-vs2013 |
I think you misinterpreted the tuple proposal:
Tuple deconstruction is also general purpose and I believe that the compiler will allow positional deconstruction with any type that resolves an instance (or extension) method |
@HaloFour Syntax around existing framework types is good. I think my misunderstanding on that point was based on this section of #347 and the discussion that followed:
But it's not clear to me how the proposed syntax could support both System.Tuple and System.ValueTuple. What type does "point" have in the statement Also, if it does in fact work with both kinds of tuple types, then what is the benefit of restricting the syntax to two specific types, versus a general-purpose syntax that works with any type that follows a specific pattern? In regards to tuple deconstruction, I thought this had been put on the back burner for now so I left it out: (from #3913)
However, since we're talking about deconstruction there is one minor issue I have with the // given this signature
public (Dog dog, DogFood food) GetDogAndFood() { .. }
// this is not allowed:
(Animal a, Food f) = GetDogAndFood();
// but this is:
var result = GetDogAndFood();
Animal a = result.dog;
Food f = result.food; I understand why tuple types cannot be covariant, but when the values are deconstructed into local variables, this restriction is surprising and unfortunate. Getting off topic here, but if you are set on a |
That would result in a
There was the potential that if you explicitly specified the result type that tuple creation syntax could create that type, but I don't know if that was adopted. System.Tuple<int, string> tuple = (123, "foo");
void AcceptsTuple(KeyValuePair<string, int> pair) { }
AcceptsTuple(("foo", 123));
Unfortunately the various proposals and design notes are quite old. Poking around at newer issues shows that deconstruction is in the works. For example #12635 explicitly mentions changing the signature pattern of the |
var x = ();
var y = ("asdf", 1.0);
Whatever x = blah;
Whatever y = (blah); doesn't only just consult Whatever x = (1, 2); ever consider implicit conversion operators? e.g. what if
Whatever foo = Random.CoinFlip() ? (1, 2) : (1, 2, 3); |
var x = ();
var y = ("asdf", 1.0); These two lines would produce a compiler error, according to the proposal. The point is that value lists do not have a type; they are not tuples. They behave the same as lambda expressions, for which this is a compiler error as well: var x = (x, y) => x + y; With respect to implicit conversions, no, they have no effect here because value lists do not have a type to begin with. Whatever foo = Random.CoinFlip() ? (1, 2) : (1, 2, 3); This would be a compiler error as well, just as lambda expressions cannot be used in the ternary conditional operator without a cast. You would need to write it like this instead: var foo = Random.CoinFlip() ? (Whatever)(1, 2) : (Whatever)(1, 2, 3); |
So tuple expressions default to ValueTuple, but you can cast them to other types as well. Ok. But would you agree that's not really a "syntax around both the existing System.Tuple types and the newer System.ValueTuple types", rather a special case of compiler-generated implicit casting?
Actually #12635 is referring to the type of a deconstruction expression, not the return type of the Deconstruct method. |
I'm not finding much of anything buried in design notes around tuple literals being used to create other types. I have no idea if that concept is actually being implemented. So, until I find out otherwise, tuple literals always create However, deconstruction can work with any types.
That it does, I had misread the very short summary of the issue and interpreted it as a design change. I'm sure you noticed a frustration here about the lack of design notes. |
We're pretty much committed to tuples for C# 7, so we won't be doing anything "in lieu of" them. |
public delegate TReturn Func<[NamedArgument] T1, [NamedArgument] T2, TReturn>(
[NameOfGenericArgument(1)] T1 arg1,
[NameOfGenericArgument(2)] T2 arg2);
public IEnumerable<T> Where<T>(this IEnumerable<T> sequence, Func<int index, T item, bool> predicate)
{
// here we can call "predicate" using named parameters:
// predicate(index: 1, item: "abc")
}
// Outside, the caller will see those arguments names in the intellisense tip
// A smart editor could insert meaningful argument names upon "tab" completion:
items.Where( [[Tab]]
items.Where((index, item) =>
static Color Adjust(this Color color, Func<double, double, double, double, Color> adjustment) { ... }
// vs:
static Color Adjust(this Color color, Func<double red, double green, double blue, double alpha, Color result> adjustment) { ... } I find these ideas to be resonant. |
Attempting to provide argument names to the generic parameters would just be messy. For starters, there's no correlation between the generic type parameters and the parameters of the actual delegate. Even given the suggestion to use attributes on the delegate definition how would you handle a delegate that used a single generic type parameter for more than one argument? The compiler would have to be pretty strict about how the associations are made, and then the benefit would only really apply to the smallest subset of possible delegates anyway. When there's ever any doubt I just define a new delegate. It seems pretty unnecessary to come up with a way to decorate the generic delegates with names when defining new delegates is just as simple and not that much more verbose. public delegate Color AdjustmentDelegate(double red, double green, double blue, double alpha);
static Color Adjust(this Color color, AdjustmentDelegate adjustment) { ... } |
@HaloFour The only purpose would be to provide additional documentation. It would not be attached to the delegate definition but rather to the delegate parameter as part of the method to which the delegate is in an argument.
I would very much prefer to use generic delegates unless I specifically want to prevent someone from passing a delegate stored in a reference which is most often a IReadOnlyList<int> GetNumbers()
{
Predicate<int> even = n => n % 2 == 0;
IEnumerable<int> numbers = GetValues();
if (numbers is List<int> list)
{
return list.FindAll(even);
}
return numbers
.Where(even) // error
.ToList();
} I would like to avoid creating more cases like that. |
Related issues: #98, #347, #3913
Most of the talk about tuples in C# 7 has been focused on providing language support for compiler-generated tuple types or something like
System.ValueTuple
. In this proposal I will suggest two alternative, and more generalized, features that are designed to work together to provide nearly the same benefit as the previous tuple proposals, but with additional flexibility that allows the new syntax to be used with existing class models, previous versions of the CLR, and other programming languages.These are the two language features being proposed. They can be implemented and tested independently of each other, and each is generally useful on its own.
1. Untyped value lists
Parenthesized value lists, like anonymous functions, could have no type. Rather, they would be implicitly convertible to a compatible type, where a compatible type is defined as one having a constructor overload callable using the values in the list. This is roughly how lambda expressions are specified, and while it may not lead to succinctness on par with more functional languages, it is extremely flexible and transparent. For example, the flexibility of this design has allowed the same lambda expression syntax to construct instances of both delegates and query expressions.
Untyped parenthesized lists are more generally useful because they can be used with all kinds of types, not just tuples. For example:
With untyped value lists, we no longer need to decide whether tuple syntax should create reference or value types—it is simply determined by the target type, and therefore up to the developer. Nor do we need to choose between mutable and immutable tuple types. In cases where mutable tuples are appropriate, or reference type tuples are determined to be a performance bottleneck, it is relatively easy to change a method signature use ValueTuple instead of Tuple and continue using the same construction syntax. This allows developers to even define and use their own "tuple" types where appropriate, customizing
GetHashCode()
andEquals()
, implementing interfaces, etc.Optional features
Here are some additional syntax ideas for this feature, though I don't see a strong enough use case for any of them to be implemented (at least not right away).
()
would be equivalent to (and shorter than)default(TypeName)
or evennew TypeName()
.2. Generic argument name annotations
When constructing a generic type, an optional identifier could be allowed to appear after each type argument. For example:
The compiler can use the optional identifier to rename certain members of the type. In the case above, the compiler has renamed the properties of Tuple with the names provided in the type arguments. This could be made possible by applying various attributes to the type itself that tell the compiler where to use the name. In the case of
Tuple<T1,T2>
, it might look something like this:The compiler would not generate any new types, of course, rather the renamed members would simply be accessible with a different name.
This is useful for all sorts of types. For example, adding annotations to
KeyValuePair<TKey,TValue>
would allow a user to apply meaningful names to theKey
andValue
properties. Ideally, name annotations would flow through the type inferencing algorithm, allowing cases like this:Use with generic delegates
Delegate types could also be annotated using the same syntax:
Then we can apply meaningful argument names to delegate functions. For example, an overload of LINQ's "Where" method might look like this:
One possible "social" benefit of this feature is that it will encourage developers to reuse framework types where appropriate, rather than declaring their own, leading to more consistent and compact code. At this point, a member or parameter having a type of Func or Action with more than about 3 arguments is quite unwieldily. Consider:
Persisting name annotations in signatures
Argument name annotations that appear in member signatures would need to be persisted somehow in metadata. This was addressed for tuples in #3913, and here is how it could be implemented more generally for generic argument name annotations:
Compared to the other proposed syntax
Despite
Tuple<int X, int Y>
being more verbose than(int x, int y)
, the semantics of the shorter syntax are not obvious. Is it a reference type or value type? Can it be marshalled to native code, and is it blittable? Is it serializable? Do those identifiers represent fields or properties, and are they readonly or volatile? If they are mutable fields, can they be passed by reference? If I pass one field by reference, or capture it in a lambda, does this prevent the other one from being collected by the GC? I am sure the design team is capable of arriving at a best-case answer for all these questions. But an ordinary programmer, even an experienced programmer, will certainly not immediately know the answer to these questions by looking at(int x, int y)
. He will when he seesTuple<int X, int Y>
.Optional features
NameOfGenericArgumentAttribute
could be extended to accept a kind of template string instead of a simple generic argument number, allowing multiple members to be customized with the same argument name. This offers some interesting possibilities, take for example this generic immutable type, which provides "Set" methods to transform itself (also note the use of untyped value lists in the "Set" methods):The name of the members of this class could be customized by its creator:
What's wrong with the current tuple proposal?
System.Tuple
,System.ValueTuple
, anonymous types, and maybe records. None of these are compatible with each other; each has its own syntax and (hard-coded) semantics.Language syntax that is usable with all types that follow a certain structure is more generally useful than syntax that is restricted to a specific type. C# has followed this principle to advantage in the past and I believe it is natural to continue to do so. For example:
foreach
does not require an object to implementIEnumerable
, only that it declares aGetEnumerator()
methodfrom..in..select
does not require an instance to implement any kind of interface, only to have instance methods in scope with certain names and signaturesawait
does not define or require any kind of IAwaitable interfaceSometimes linking a feature to a specific type is unavoidable (for example, generators are required to return some kind of
IEnumerable<T>
, and async methods that return a value must returnTask<T>
). But where we can avoid this, indeed we should.Perhaps if we were starting from scratch, and there was no existing .NET framework, it would make sense to bind the tuple syntax to a specific tuple type that was used throughout the framework. Or, if C# was more functional in nature and specifically required tuple constructors to be as short as possible, it would make sense to bind to a specific tuple type. But this is not our situatation. C# has never been the most succinct language on the block, rather it has excelled in finding a good balance between being succinct and explicit.
This proposal requires very few changes to the .NET class library, no changes to the runtime, and could provide an immediate benefit to authors both targeting and consuming previous versions of the framework.
The text was updated successfully, but these errors were encountered: