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

C# Design Notes for Apr 6, 2016 #10429

Closed
MadsTorgersen opened this issue Apr 8, 2016 · 95 comments
Closed

C# Design Notes for Apr 6, 2016 #10429

MadsTorgersen opened this issue Apr 8, 2016 · 95 comments

Comments

@MadsTorgersen
Copy link
Contributor

C# Design Notes for Apr 6, 2016

We settled several open design questions concerning tuples and pattern matching.

Tuples

Identity conversion

Element names are immaterial to tuple conversions. Tuples with the same types in the same order are identity convertible to each other, regardless of the names.

That said, if you have an element name at one position on one side of a conversion, and the same name at another position on the other side, you almost certainly have bug in your code:

(string first, string last) GetNames() { ... }
(string last, string first) names = GetNames(); // Oops!

To catch this glaring case, we'll have a warning. In the unlikely case that you meant to do this, you can easily silence it e.g. by assigning through a tuple without names at all.

Boxing conversion

As structs, tuples naturally have a boxing conversion. Importantly, the names aren't part of the runtime representation of tuples, but are tracked only by the compiler. Thus, once you've "cast away" the names, you cannot recover them. In alignment with the identity conversions, a boxed tuple will unbox to any tuple type that has the same element types in the same order.

Target typing

A tuple literal is "target typed" whenever possible. What that means is that the tuple literal has a "conversion from expression" to any tuple type, as long as the element expressions of the tuple literal have an implicit conversion to the element types of the tuple type.

(string name, byte age) t = (null, 5); // Ok: the expressions null and 5 convert to string and byte

In cases where the tuple literal is not part of a conversion, it acquires its "natural type", which means a tuple type where the element types are the types of the constituent expressions. Since not all expressions have types, not all tuple literals have a natural type either:

var t = ("John", 5); // Ok: the type of t is (string, int)
var t = (null, 5); //   Error: null doesn't have a type

A tuple literal may include names, in which case they become part of the natural type:

var t = (name: "John", age: 5); // The type of t is (string name, int age)

Conversion propagation

A harder question is whether tuple types should be convertible to each other based on conversions between their element types. Intuitively it seems that implicit and explicit conversions should just "bleed through" and compose to the tuples. This leads to a lot of complexity and hard questions, though. What kind of conversion is the tuple conversion? Different places in the language place different restrictions on which conversions can apply - those would have to be "pushed down" as well.

var t1 = ("John", 5);   // (string, int)
(object, long) t2 = t1; // What kind of conversion is this? Where is it allowed

On the whole we think that, while intuitive, the need for such conversions isn't actually that common. It's hard to construct an example that isn't contrived, involving for instance tuple-typed method parameters and the like. When you really need it, you can deconstruct the tuple and reconstruct it with a tuple literal, making use of target typing.

We'll keep an eye on it, but for now the decision is not to propagate element conversions through tuple types. We do recognize that this is a decision we don't get to change our minds on once we've shipped: adding conversions in a later version would be a significant breaking change.

Projection initializers

Tuple literals are a bit like anonymous types. The latter have "projection initializers" where if you don't specify a member name, one will be extracted from the given expression, if possible. Should we do that for tuples too?

var a = new { Name = c.FirstName, c.Age }; // Will have members Name and Age
var t = (Name: c.FirstName, c.Age); // (string Name, int Age) or error?

We don't think so. The difference is that names are optional in tuples. It'd be too easy to pick up a random name by mistake, or get errors because two elements happen to pick up the same name.

Extension methods on tuples

This should just work according to existing rules. That means that extension methods on a tuple type apply even to tuples with different element names:

static void M(this (int x, int y) t) { ... }

(int a, int b) t = ...;
t.M(); // Sure

Default parameters

Like other types, you can use default(T) to specify a default parameter of tuple type. Should you also be allowed to specify a tuple literal with suitably constant elements?

void M((string, int) t = ("Bob", 7)) { ... } // Allowed?

No. We'd need to introduce a new attribute for this, and we don't even know if it's a useful scenario.

Syntax for 0-tuples and 1-tuples?

We lovingly refer to 0-tuples as nuples, and 1-tuples as womples. There is already an underlying ValueTuple<T> of size one. We should will also have the non-generic ValueTuple be an empty struct rather than a static class.

The question is whether nuples and womples should have syntactic representation as tuple types and literals? () would be a natural syntax for nuples (and would no doubt find popularity as a "unit type" alternative to void), but womples are harder: parenthesized expressions already have a meaning!

We made no final decisions on this, but won't pursue it for now.

Return tuple members directly in scope

There is an idea to let the members of a tuple type appearing in a return position of a method be in scope throughout the method:

(string first, string last) GetName()
{
  first = ...; last = ...; // Assign the result directly
  return;                  // No need to return an explicit value
}

The idea here is to enhance the symmetry between tuple types and parameter lists: parameter names are in scope, why should "result names"?

This is cute, but we won't do it. It is too much special casing for a specific placement of tuple types, and it is also actually preferable to be able to see exactly what is returned at a given return statement.

Integrating pattern matching with is-expressions and switch-statements

For pattern matching to feel natural in C# it is vital that it is deeply integrated with existing related features, and does in fact take its queues from how they already work. Specifically we want to extend is-expressions to allow patterns where today they have types, and we want to augment switch-statements so that they can switch on any type, use patterns in case-clauses and add additional conditions to case-clauses using when-clauses.

This integration is not always straightforward, as witnessed by the following issues. In each we need to decide what patterns should generally do, and mitigate any breaking changes this would cause in currently valid code.

Name lookup

The following code is legal today:

if (e is X) {...}
switch(e) { case X: ... }

We'd like to extend both the places where X occurs to be patterns. However, X means different things in those two places. In the is expression it must refer to a type, whereas in the case clause it must refer to a constant. In the is expression we look it up as a type, ignoring any intervening members called X, whereas in the case clause we look it up as an expression (which can include a type), and give an error if the nearest one found is not a constant.

As a pattern we think X should be able to both refer to a type and a constant. Thus, we prefer the case behavior, and would just stop giving an error when case X: refers to a type. For is expressions, to avoid a breaking change, we will first look for just a type (today's behavior), and if we don't find one, rather than error we will look again for a constant.

Conversions in patterns

An is expression today will only acknowledge identity, reference and boxing conversions from the run-time value to the type. It looks for "the actual" type, if you will, without representation changes:

byte b = 5;
WriteLine(b is byte);           // True:  identity conversion
WriteLine((object)b is byte);   // True:  boxing conversion
WriteLine((object)b is object); // True:  reference conversion
WriteLine(b is int);            // False: numeric conversion changes representation

This seems like the right semantics for "type testing", and we want those to carry over to pattern matching.

Switch statements are more weird here today. They have a fixed set of allowed types to switch over (primitive types, their nullable equivalents, strings). If the expression given has a different type, but has a unique implicit conversion to one of the allowed ones, then that conversion is applied! This occurs mainly (only?) when there is a user defined conversion from that type to the allowed one.

That of course is intended only for constant cases. It is not consistent with the behavior we want for type matching per the above, and it is also not clear how to generalize it to switch expressions of arbitrary type. It is behavior that we want to limit as much as possible.

Our solution is that in switches only we will apply such a conversion on the incoming value only if all the cases are constant. This means that if you add a non-constant case to such a switch (e.g. a type pattern), you will break it. We considered more lenient models involving applying non-constant patterns to the non-converted input, but that just leads to weirdness, and we don't think it's necessary. If you really want your conversion applied, you can always explicitly apply it to the switch expression yourself.

Pattern variables and multiple case labels

C# allows multiple case labels on the same body. If patterns in those case labels introduce variables, what does that mean?

case int i:
case byte b:
    WriteLine("Integral value!");
    break;

Here's what it means: The variables go into the same declaration space, so it is an error to introduce two of the same name in case clauses for the same body. Furthermore, the variables introduced are not definitely assigned, because the given case clause assigns them, and you didn't necessarily come in that way. So the above example is legal, but the body cannot read from the variables i and b because they are not definitely assigned.

It is tempting to consider allowing case clauses to share variables, so that they could be extracted from similar but different patterns:

case (int age, string name):
case (string name, int age):
    WriteLine($"{name} is {age} years old.");
    break;

We think that is way overboard right now, but the rules above preserve our ability to allow it in the future.

Goto case

It is tempting to ponder generalizations of goto case x. For instance, maybe you could do the whole switch again, but on the value x. That's interesting, but comes with lots of complications and hidden performance traps. Also it is probably not all that useful.

Instead we just need to preserve the simple meaning of goto case x from current switches: it's allowed if x is constant, if there's a case with the same constant, and that case doesn't have a when clause.

Errors and warnings

Today 3 is string yields a warning, while 3 as string yields and error. They philosophy seems to be that the former is just asking a question, whereas the other is requesting a value. Generalized is expressions like 3 is string s are sort of a combination of is and as, both answering the question and (conditionally) producing a value. Should they yield warnings or errors?

We didn't reach consensus and decided to table this for later.

Constant pattern equality

In today's switch statement, the constants in labels must be implicitly convertible to the governing type (of the switch expression). The equality is then straightforward - it works the same as the == operator. This means that the following case will print Match!.

switch(7)
{
  case (byte)7:
    WriteLine("Match!");
    break;
}

What should be the case if we switch on something of type object instead?:

switch((object)7)
{
  case (byte)7:
    WriteLine("Match!");
    break;
}

One philosophy says that it should work the same way regardless of the static type of the expression. But do we want constant patterns everywhere to do "intelligent matching" of integral types with each other? That certainly leads to more complex runtime behavior, and would probably require calling helper methods. And what of other related types, such as float and double? There isn't similar intelligent behavior you can do, because the representations of most numbers will differ slightly and a number such as 2.1 would thus not be "equal to itself" across types anyway.

The other option is to make the behavior different depending on the compile-time type of the expression. We'll use integral equality only if we know statically which one to pick, because the left hand side was also known to be integral. That would preserve the switch behavior, but make the pattern's behavior dependent on the static type of the expression.

For now we prefer the latter, as it is simpler.

Recursive patterns

There are several core design questions around the various kinds of recursive patterns we are envisioning. However, they seem to fall in roughly two categories:

  1. Determine the syntactic shape of each recursive pattern in itself, and use that to ensure that the places where patterns can occur are syntactically well-formed and unambiguous.
  2. Decide exactly how the patterns work, and what underlying mechanisms enable them.

This is an area to focus more on in the future. For now we're just starting to dig in.

Recursive pattern syntax

For now we envision three shapes of recursive patterns

  1. Property patterns: Type { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }
  2. Tuple patterns: (Pattern_1, ... Pattern_n)
  3. Positional patterns: Type (Pattern_1, ... Pattern_n)

There's certainly room for evolution here. For instance, it is not lost on us that 2 and 3 are identical except for the presence of a type in the latter. At the same time, the presence of a type in the latter seems syntactically superfluous in the cases where the matched expression is already known to be of that type (so the pattern is used purely for deconstruction and/or recursive matching of the elements). Those two observations come together to suggest a more orthogonal model, where the types are optional:

  1. Property patterns: Type_opt { Identifier_1 is Pattern_1, ... , Identifier_n is Pattern_n }
  2. Positional patterns: Type_opt (Pattern_1, ... Pattern_n)

In this model, what was called "tuple patterns" above would actually not just apply to tuples, but to anything whose static type (somehow) specifies a suitable deconstruction.

This is important because it means that "irrefutable" patterns - ones that are known to always match - never need to specify the type. This in turn means that they can be used for unconditional deconstruction even in syntactic contexts where positional patterns would be ambiguous with invocation syntax. For instance, we could have what would amount to a "deconstruction" variant of a declaration statement, that would introduce all its match variables into scope as local variables for subsequent statements:

(string name, int age) = GetPerson();    // Unconditional deconstruction
WriteLine($"{name} is {age} years old"); // name and age are in scope

How recursive patterns work

Property patterns are pretty straightforward - they translate into access of fields and properties.

Tuple patterns are also straightforward if we decide to handle them specially.

Positional patterns are more complex. We agree that they need a way to be specified, but the scope and mechanism for this is still up for debate. For instance, the Type in the positional pattern may not necessarily trigger a type test on the object. Instead it may name a class where a more general "matcher" is defined, which does its own tests on the object. This could be complex stuff, like picking a string apart to see if it matches a certain format, and extracting certain information from it if so.

The syntax for declaring such "matchers" may be methods, or a new kind of user defined operator (like is) or something else entirely. We still do not have consensus on either the scope or shape of this, so there's some work ahead of us.

The good news is that we can add pattern matching in several phases. There can be a version of C# that has pattern matching with none or only some of the recursive patterns working, as long as we make sure to "hold a place for them" in the way we design the places where patterns can occur. So C# 7 can have great pattern matching even if it doesn't yet have all of it.

@i3arnon
Copy link

i3arnon commented Apr 8, 2016

We should will also have the non-generic

I'm assuming it's either should or will?

...while 3 as string yields and error.

an error?

@keichinger
Copy link

Re womples: Wouldn't be this kind of hard and very ambiguous with the existing syntax for casting? I mean it's super easy to misread a tuple declaration/definition as type cast.

@HaloFour
Copy link

HaloFour commented Apr 8, 2016

Identity conversion

The comparison between tuples and argument lists have been made several times, so why wouldn't you treat named tuples the same as you would named argument lists? I'd make it a compiler error to use the wrong name at all, and if you reorder the names I would reorder the values in the tuple silently. Want to avoid the error, don't use names or use an intermediary unnamed tuple. This is exactly how Swift works and I think that it's more than reasonable.

(string first, string last) tuple1 = ("Bill", "Gates");
(string last, string first) tuple2 = tuple1;
Debug.Assert(tuple1.first == tuple2.first && tuple1.last == tuple2.last);

(string f, string l) tuple3 = tuple1; // compiler error, message akin to CS1739

(string, string) tuple4 = tuple1;
(string f, string l) tuple5 = tuple4;
Debug.Assert(tuple1.first == tuple5.f && tuple1.last == tuple5.l);
(string last, string first) tuple6 = tuple4;
Debug.Assert(tuple1.first == tuple6.last && tuple1.last == tuple6.first);

Swift for comparison:

let tuple1: (first: String, last: String) = ("Steve", "Jobs")
let tuple2: (last: String, first: String) = tuple1

assert(tuple1.first == tuple2.first && tuple1.last == tuple2.last)

// error: cannot convert value of type '(first: String, last: String)' to specified type '(f: String, l: String)'
let tuple3: (f: String, l: String) = tuple1

let tuple4: (String, String) = tuple1
let tuple5: (f: String, l: String) = tuple4
assert(tuple1.first == tuple5.f && tuple1.last == tuple5.l)
let tuple6: (last: String, first: String) = tuple4
assert(tuple1.first == tuple6.last && tuple1.last == tuple6.first)

Syntax for 0-tuples and 1-tuples?

For 0-tuples I'd make the functional folks happy and just add a System.Unit struct to the BCL. As for 1-tuples, is it worth wrapping them in anything? Would it be difficult for the compiler to just treat them as their underlying type? If so, ValueTuple<T> seems fine. I assume that a 0-tuple would be (), but what would a 1-tuple be? (x,)?

Update: Swift treats () as the type Void which can have no value. Swift also treats 1-tuples as the same as the type of its only element, so the types (Int), (((((Int))))) and Int are all the same, which works well with expressions as (1), (((((1))))) and 1 are all int of the same value in C#.

Return tuple members directly in scope

I don't think it's worth it. Smells like VB5/6 function syntax which had the pit of failure of easily forgetting to actually return if you set the value in a condition.

Pattern variables and multiple case labels

I hope that this is revisited along with OR/AND patterns. The ability to define two different patterns with compatible sets of variable patterns would be very powerful. F# has this capability.

Goto case

So this would mean that you could use goto case 6; but only if case 6: exists? That's disappointing. There was something ... elegant about #7703 (comment).

Recursive patterns

I'm going to poke over this section some more.

@bbarry
Copy link

bbarry commented Apr 8, 2016

@HaloFour

So this would mean that you could use goto case 6; but only if case 6: exists? ...

I think there is a certain aspect of goodness in the current design as of this issue in that it doesn't prevent that from being changed eventually.

@HaloFour
Copy link

HaloFour commented Apr 8, 2016

Recursive patterns

I like the idea of making the type optional. This should allow for property patterns to be applied to anonymous types, which were an open issue in the spec.

I understand not wanting to rush into positional deconstruction (aside tuple types). The proposed user-defined operator is had a lot of power but also a lot of limitations. I think that evaluating them alongside ADTs and DUs would be a good idea so that the experience of defining and matching against those types is intuitive and fluid. Maybe active patterns as well.

For the unconditional deconstruction example, no let required?

@bbarry If patterns are matched in lexical order but goto case 6; jumps immediately to case 6 then adding the behavior later could be a breaking change:

public static class MyNumberPattern {
    public static bool operator is(int number) {
        // some matching logic here
    }
}

int operand = SomeNumber();
switch (operand) {
    case MyNumberPattern():
        // do something here
        break;
    case 6:
        // do something here
        break;
    default:
        Console.WriteLine("none of the above!");
        goto case 6; // jump immediately to case 6, or evaluate expression 6 against MyNumberPattern?
}

@alrz
Copy link
Contributor

alrz commented Apr 9, 2016

Projection initializers: Can't we just "turn it on" when we actually want a named tuple? In that case an error is desirable if (1) expression does not have an extractable name or (2) names are duplicated, e.g.

// returns (string FirstName, int Age)
var t = (: c.FirstName, c.Age);
// error
var t = (: p.Name, c.Name);

This would be extremely useful in LINQ when you don't want to allocate anonymous types.

Default parameters: I think #7737 can be a good addition to avoid double parents like default((int,int)) in these cases. Alternatively you can special case it like default(int, int). Also for switch switch(1, 2) and casts (int, int) o.

Multiple case labels: I would like to see this for match even if variables can't be shared just yet.

@Suchiman
Copy link
Contributor

Suchiman commented Apr 9, 2016

About recursive patterns and multiple case labels

Consider you have code of similiar shape that share no common parent class or interface

class PersonV1
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool IsEvil { get; set; }
}

class PersonV2
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool IsNotVeryNice { get; set; }
}

Right now the only way to deal with this is to write two methods that copy the same code, or write a wrapper.
But this could be our great chance for structural typing!

Disclaimer: Forgive me if i haven't got the correct syntax for it, i find it hard to get an overview of the complete currently proposed syntax for pattern matching (if you're not familiar with parser generator grammar syntax, patterns.md is not easy to read).

switch (somePerson)
{ 
    case PersonV1 { FirstName, LastName } person:
    case PersonV2 { FirstName, LastName } person:
        WriteLine($"Got a Person, it's {person.FirstName} {person.LastName}");
        person.FirstName = "John"; 
        break;
}

This would require that all specified property names have equal types or both cases can be matched against the same pattern (like AND patterns):

class PersonV1
{
    public object Boss { get; set; }
}

class PersonV2
{
    public MightyBoss Boss { get; set; }
}

class MightyBoss
{
    public string Name { get; set; }
}

switch (somePerson)
{ 
    case PersonV1 { Boss is MightyBoss } person:
    case PersonV2 { Boss is MightyBoss } person:
        WriteLine($"Got a Person, his Boss is {person.Boss.Name}");
        break;
}

Could be even "less code" by just specifying the type names and letting the match be the intersection of identical properties (name, type, accessibility).

switch (somePerson)
{ 
    case PersonV1 person:
    case PersonV2 person:
        WriteLine($"Got a Person, it's {person.FirstName} {person.LastName} !");
        person.FirstName = "John"; 
        break;
}

@alrz
Copy link
Contributor

alrz commented Apr 9, 2016

Conversions in patterns: If I undrestand this correctly, so the following is possible?

switch(new Num(5)) {
  case P.Even: break;
  case P.Odd: break;
}

enum P { Even, Odd }
class Num {
  readonly int value;
  public Num(int value) { this.value = value; }
  public static implicit operator P(Num num) {
    return num.value % 2 == 0 ? P.Even : P.Odd;
  }
}

Which with extension operators and #952 becomes:

switch(5) {
  case Even: break;
  case Odd: break;
}

enum P { Even, Odd }
static class IntegerExtensions {
  public static implicit operator P(this int num) {
    return num % 2 == 0 ? Even : Odd;
  }
}

But the following statement,

Our solution is that in switches only we will apply such a conversion on the incoming value only if all the cases are constant.

Makes this one impossible.

switch(5) {
  case Case1(var data): break;
  case Case2(var data): break;
}

// #6739
enum class P { Case1(int Data), Case2(int Data) }
static class IntegerExtensions {
  public static implicit operator P(this int num) {
    return num % 2 == 0 ?  Case1(5) : Case2(6);
  }
}

I expect this to work and the conversion operator execute once. This is basically what F# features via active patterns.

@orthoxerox
Copy link
Contributor

I think that womples should be implicitly convertible to and from the corresponding scalar values. This way (2) is still an int, but it can be used anywhere a ValueTuple<int> is required. An (int) can still be a tuple type, but that will surely complicate the parser further: (int)a(int b) looks like a cast until you get to the argument list, I think Roslyn currently has one token look-ahead to check if a parenthesized expression is a cast.

@JamesNK
Copy link
Member

JamesNK commented Apr 10, 2016

Hello! It's me, the JSON serializer guy, with serializer guy type questions:

This has probably been covered elsewhere but will could you give an example of what the generated C# will look like for a new tuple type, e.g. (string first, string last)

Will it be similar to what an anonymous class generates and have a constructor with parameters of the same names and getter only properties? (although struct instead of class and I'm guessing overridden Equals/GetHashcode impls)

If you follow that approach then the new tuples should be serialized and deserialized by Json.NET without any changes

@orthoxerox
Copy link
Contributor

@JamesNK tuple members will not have names after compilation. Tuples'll all belong to the new tuple types that look like existing tuple types, except they are mutable structs, e.g.

public struct ValueTuple<T1, T2> : IEquatable<ValueTuple<T1, T2>>
{
    public T1 Item1;
    public T1 Item2;
    public ValueTuple(T1 item1, T1 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }

    //Equals, GetHashCode and all that jazz
}

At least that's what's been demoed at Build.

@JamesNK
Copy link
Member

JamesNK commented Apr 10, 2016

So the tuple property names aren't available via reflection?

@orthoxerox
Copy link
Contributor

@JamesNK no, you'll get ItemN.

@axel-habermaier
Copy link
Contributor

@MadsTorgersen: Is what @orthoxerox said true? Tuple names are not preserved across assembly boundaries? I was under the impression that attributes would be used to encode tuple names in method signatures, for instance. Has that decision changed?

@HaloFour
Copy link

@axel-habermaier They should be "preserved" in boundaries such as method parameters/return values, properties and fields through said attributes. So if you were to pass a declared object (or maybe anonymous type) with a named tuple then JSON.NET could negotiate the attributes attached to the property. But if you were to just create a tuple instance and hand it off to JSON.NET directly there would be nowhere to put that metadata, e.g.:

(string first, string last) tuple = ("Bill", "Gates");
string json = JsonConvert.SerializeObject(tuple);

@axel-habermaier
Copy link
Contributor

@HaloFour: It wouldn't work with anonymous types, though? Their properties are usually of generic type, unless that is changed for tuples to encode item names.

@HaloFour
Copy link

@axel-habermaier You're right, that would probably preclude being able to inspect the names from the properties of anonymous types unless the compiler were to start emitting those shared types differently. I'd say that the ephemeral nature of tuples should discourage their use in a serialized form but it's inevitable that this is a situation that will create confusion.

@GeirGrusom
Copy link

@JamesNK I think you have to encode tuples as arrays in JSON. The names are primarily for documentation purposes.

@DavidArno
Copy link

@orthoxerox,

Hopefully, since there is talk in the F# community of adopting these struct tuples in F# as well as C#, the silly idea of making them mutable will be quietly killed off...

@DavidArno
Copy link

Whilst I really like the idea of a "nuple" type, I think it right to not rush into implementing it. Properly thought through and done well, it could be a huge boon to the language.

For example, I'd expect not to have put a return into a nuple-returning method, eg:

() F(object o) => Console.WriteLine(o);

Following on from that, if I declare a lambda, (o) => Console.WriteLine(o), that could have the signature Func<Object, Nuple>, and so ought to be mappable to such a delegate. Just changing the signature though would presumably be a breaking change, as it would no longer map to Action<object>.

To really unleash the power of Nuple, then Action<T> and Func<T, Nuple> ought to be interchangeable, which would mean:

void F(object o) => Console.WriteLine(o);

could also be treated as meeting the Func<Object, Nuple> contract and thus being able to use match expressions with void methods would just fall out the wash:

void SomeAction(Option<T> option) =>
    option match (
        case Some<T>(var value) : Console.WriteLine(value)
        case None<T>() : Console.WriteLine("None)
    );

being the equivalent to, and interchangeable with,

() SomeAction(Option<T> option) =>
    option match (
        case Some<T>(var value) : (value) => { Console.WriteLine(value); return (); }
        case None<T>() : () => { Console.WriteLine("None); return (); }
    );

@paulomorgado
Copy link

Target Typing

var t = (null, 5); // Error: null doesn't have a type

But this won't be an error:

var t = ((string)null, 5);

Right?

Syntax for 0-tuples and 1-tuples

From the top of my mind, I don't know if () will be a breaking change or not (I don't think it is) or how hard will it be to handle by the compiler.

As for womples, the unnamed ones is almost impossible to get, but I don't think there's a problem with named usage:

(int a) t = GetWomple();

@benjamin-hodgson
Copy link

I think tuples are awesome. I've been itching for exactly this feature for years. I have a few questions, though. (Not necessarily requesting these features, just enquiring after their status 😉)

(Im-)Mutability

What's the plan for mutability of tuples? I haven't seen a definitive answer.

If tuples are going to be structs, which I'm broadly in favour of, they really (really really) should be immutable. Mutable structs are hard to use and go against Microsoft's own design guidelines. It'd be a big mistake to build a mutable struct into the language like this.

If they're going to be classes I still think they should be immutable - I don't know of any other language which features mutable tuples, and I don't believe there's any good reason for them to be mutable.

Integration with System.Tuple, compatibility with existing code

Do you intend to provide any form of integration with System.Tuple? This seems like it could be tricky, at least because System.Tuple is a reference type but these tuples will be value types. But a backwards-compatible way to use existing code such as Enumerable.Zip with the new tuple types would be desirable:

foreach ((var x, var y) in xs.Zip(ys))
{
    // ...
}

Variance

Assuming tuples will be immutable, is there a plan to make them covariant in their type parameters? One thing I find tedious about System.Tuple is the need to specify the type at creation if I'm planning to upcast it:

Tuple<IEnumerable<int>> GetInts()
{
    var ints = new List<int>();
    return new Tuple<IEnumerable<int>>(ints);
    // or
    return Tuple.Create((IEnumerable<int>)ints);
    // or
    return Tuple.Create<IEnumerable<int>>(ints);
    // but not what I'd like to write, which is
    return Tuple.Create(ints);
}

I'm aware that this would likely require CLR changes, and that many (most?) of the use-cases are covered by your existing "target typing" design, so this is unlikely to happen. But variance is, in my view, a more principled and scalable way of solving the problem.

Integration with object expressions

Is there a plan to provide any form of integration between tuples and anonymous object expressions? This one's a long shot because I don't know what such an integration would look like and I can't think of any meaningful use-cases for it. But these two look pretty similar:

var x = new { Name = "John", Age = 5 };
var y = (name: "John", age: 5);

The tuple version is superior because you can refer directly to the type of a tuple (for example, in the return type of a method) whereas the anonymous object can only be used implicitly with var or implicitly converted to object. (That said, tuples clearly don't supersede anonymous objects altogether - anonymous objects are reference types and tuples are value types, and anonymous objects will presumably still be used in the desguaring of LINQ query expressions.)

@smoothdeveloper
Copy link

@DavidArno is right about making tuples immutable, and eventually provide way to build a new tuple by substituting a subset of members with new values.

The runtime/compiler should optimize behind the scene, and performance oriented code shouldn't use tuples anyways but more explicit data structure.

That said, the most of C# approach is enabling mutable state by default, so I tuple support coming with this stays close to the overal language.

It is a great thing that team working on roslyn based language and F# are taking directions to (not that it was not the case at all before) take interop as important feature (without denaturing the strengths of respective languages).

Edit: I was surprised that demos given at //Build 2016 were using mutable aspect of tuples.
Edit2: http://stackoverflow.com/questions/9714815/why-tuple-is-not-mutable-in-python

@HaloFour
Copy link

@smoothdeveloper

DavidArno is right about making tuples immutable, and eventually provide way to build a new tuple by substituting a subset of members with new values.

The concern about tuples being mutable are mitigated entirely by them also being structs. That eliminates the problems of mutating shared state. Marking tuple fields as readonly or declaring locals via let makes them effectively immutable. Parameters not marked ref would still be mutable, but the changes would not affect the caller.

@DavidArno
Copy link

@HaloFour

The concern about tuples being mutable are mitigated entirely by them also being structs

No. It is partially mitigated only. It makes it more difficult to create problems with shared state as one must use ref to mutate the original value, rather than just a copy. It absolutely does not eliminate it though.

In addition, by making them mutable by default:

  • The problems around encouraging bad behaviour - by allowing the tuple to be created, then its fields being subsequently modified - still exist.
  • It's possible that the language will support (readonly T1, readonly T2) F(... as a way of insisting on the struct's properties being immutable. This necessitates extra Tuple structs in the BCL, plus lots of extra code though in order to "do things properly".
  • Likewise, there's a vague possibility that one could use let in some way to define immutable "variables" within a block of code. or that might never come to pass (as the current pattern-matching spec prevents let being used in that way).

There's plenty of reasons not to make them mutable. And what are the reasons for making them so? Some vague suggestion that it'll make the compiler-writers lives easier to achieve performant code. As a user of their compiler, I'd prefer they made mine, and all other users' lives easier, rather than their own.

By all means provide a means of indicating that they should be mutable if the developer needs that, but that should not be the default, eg require the use of ref to indicate they can be mutated.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 15, 2016

I was implicitely referring to the distinction often made between Entities and Values.

I think there's a disconnect. And i'm a bit worried about attempting to classify things in such a manner. A tuple is neither of those things. A tuple is simply a bundling up of those things. Think of it in the same way as a parameter list. Do people deeply struggle with what a parameter list is? I don't really think so. Do they need to wonder if they should think of the parameter list as an entity or value? Not really. It's just a bunch of individual passed to you to work with.

With tuples, now that concept is being taken further and that same collection of data can be used outside of the context of passing a message. It can now, for example, be used as the result of call. Or a way of bundling together data you feel should be stored together, but which doesn't warrant the heavyweight introduction of a whole new type.

In essence, we've always been lacking that uniformity in the language. Want to decompose a single entity into multiple values? No problem. Want to pass multiple values along in a lightweight manner? No problem. Want to aggregate values together in a lightweight manner? Now you can't do it.

In my ideal world we'd have very strong unification here**. I doubt we'll get as far as I'd like (due to legacy concerns and runtime constraints). However, i view the current Tuple proposal as a reasonable step that pragmatically solves a lot of problems, while still fitting well into the C# language.

What i would love to see if this was V1:

  1. 0-tuples and void would be the same thing from a type system perspective.
  2. 1-tuples and a single value would be the same thing from the type system perspective.

What i still want to see, and which i think is doable with the current design:

  1. Clean interaction with parameter lists and tuples, making them not feel like separate ways of doing the same thing. i.e. if i have a method that returns a (int, bool) i should just be able to pass that to a method M(int, bool). Then you can have a single parameter list style that feels right to existing consumer (and which doesn't force them to create tuples), and yet you have the nice composability of being able to pass around multiple values without needing lots of overloads.

@CyrusNajmabadi
Copy link
Member

Of the only things I've seen mentioned here that probably makes sense would be to modify the C# in strict mode to warn when mutating a struct copied to a temporary local, either directly or through a property setter. Then at least the developer can be made aware that their change will not be persisted.

That would be a nifty warning. I think i would tweak it a bit. Namely we could consider a warning if you wrote into a field of a local struct and you never read from the struct afterwards. For tuples (a new type) i think we could safely add that warning. For existing structs, i think we'd either have to not have the warning, or it would need to come in some sort of 'warning wave' so that customers could opt into it.

@HaloFour
Copy link

@CyrusNajmabadi

0-tuples and void would be the same thing from a type system perspective.

I imagine that the CLR would make that a bit difficult, if not impossible. Afterall, you couldn't have a Func<()> since Func<void> isn't legal. However, if System.Unit were added in the same effort now adding ValueTuple<...> I think it makes a lot of sense to have () equate to that.

1-tuples and a single value would be the same thing from the type system perspective.

Is there a reason that this couldn't be done? Have 1-tuples simply not exist from the BCL point of view. You can define one, but it's purely syntax candy. The syntax to define a 1-tuple literal already fits perfectly given you can have any expression in parenthesis, int x = (1); being perfectly legal.

The question is could you then do (int) tuple = (1); int result = tuple.Item1; or var tuple = (x: 1); int result = tuple.x; In Swift at least while you can define 1-tuples you can't name their elements nor can you use them as tuples. (Int) is just a synonym for Int.

For tuples (a new type) i think we could safely add that warning. For existing structs, i think we'd either have to not have the warning, or it would need to come in some sort of 'warning wave' so that customers could opt into it.

That sounds like a great idea.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 16, 2016

I imagine that the CLR would make that a bit difficult, if not impossible.

Hence a V1 thing :)

However, if System.Unit were added in the same effort now adding ValueTuple<...> I think it makes a lot of sense to have () equate to that.

Then i would just be upset that Unit/Void were not the same thing :)

Is there a reason that this couldn't be done?

Getting things consistent would be tricky. It especially has conflicts around nullability/non-nullability.

For example, with the tuples today being structs, it's fine to have things like:

()? a = ...; // 0-tuple form
(string, string)? c = ...; // 2-tuple form.

Say we allow a single-type form. We would then be able to have:

()? a = ...; // 0-tuple form
(string)? b = ...; // 1-tuple form
(string, string)? c= ...; // 2-tuple form

So now you can a nullable 1-tuple. But we wouldn't allow a nullable string. So (T) is not equivalent to T. That's wonky.

We could disallow nullable 1-tuples, but now 1-tuples aren't consistent with the rest of all the tuples. etc. etc.

So, it's not like i think it's can't be done. It's more like i think it would be a lot of work, not a lot of gain TBH, and would in itself likely introduce additional issues.

The value is in 2+ tuples. Void/One are simply too baked in across both the language and runtime to make any sort of meaningful change there (without needing at least another year of work).

A very nice aspect of the existing proposals is that, as we've gone with since 2.0:

  1. This needs no runtime work.
  2. This can interoperate cleanly across versions.

I.e. you could make your tuple based APIs in C# 7.0 but still consume them from C# 3.0 code if you wanted. I think there's a virtue there that i want to keep.

@asik
Copy link

asik commented Apr 16, 2016

Namely we could consider a warning if you wrote into a field of a local struct and you never read from the struct afterwards.

I wouldn't feel too safe.

list.ForEach(item => {
    item.X = 10;
    Log(item.X.ToString());
});

Is this correct? In C#6, using conventional wisdom and following .NET design guidelines, yeah. In the next version of C#, I'll have to be more careful.

@HaloFour
Copy link

@asik

Doesn't feel any different from:

list.ForEach(item => {
    item = new Something();
});

Anyway it sounds like a very contrived case. And that same wisdom applied to C# 1.0. Unless you never put a System.Drawing.Point into a System.Collections.ArrayList?

@asik
Copy link

asik commented Apr 16, 2016

You are re-assigning a function parameter, which never changes the value passed in, regardless of value vs reference, mutable vs immutable, lambda vs method, etc. This code is incorrect in all circumstances and is just as obviously wrong as:

void Bar(string p) { p = ""; }

In my example, you cannot know whether the code is correct. It probably is if you're using C#6, so I probably wouldn't spot this as a potential issue.

Speaking of contrived examples, System.Drawing.Point is one of the few cases of Microsoft violating their own design guidelines, in this case for performance reasons. And it certainly has bitten a lot of programmers. If list is a list of System.Drawing.Point, then that code is indeed bogus, but unless this is a WinForms application there's little chance that it is, or any other mutable struct. With the current tuple proposal you get this guideline violation everywhere.

It's a very simple example using a commonly used method on a very commonly used type and it's not hard to see why you could not rely on warnings to prevent the usual data loss issues with mutable value types. The compiler cannot know your intent.

@HaloFour
Copy link

You are re-assigning a function parameter, which never changes the value passed in, regardless of value vs reference, mutable vs immutable, lambda vs method, etc.

Yep, which makes mutable struct tuples quite consistent with the semantics of an argument list.

@vladd
Copy link

vladd commented Apr 16, 2016

@CyrusNajmabadi I don't think that 1-tuple should be the same as its inner value, so this eliminates the problem with nullable 1-tuple. Just like there is no problem with a struct having single string field: the struct itself is a value type, hence one can always produce a nullable type from the said struct.

Special-casing 1-tuple makes it way too special.

Another thing is that for the convenience it might be a good idea to automagically extract the value from 1-tuple in some contexts (but I doubt that it's worth doing).

@CyrusNajmabadi
Copy link
Member

I wouldn't feel too safe.

list.ForEach(item => {
    item.X = 10;
    Log(item.X.ToString());
});

I'm not understanding the concern with this code. It would stay legal, just as it is today... Could you clarify what the issue would be with this?

@CyrusNajmabadi
Copy link
Member

This code is incorrect in all circumstances and is just as obviously wrong as:

Yes. I think providing an analyzer that would warn in this case would make a lot of sense. Just today i got PR feedback about me making this mistake in my own code. I would have definitely liked it if the system had just told me about the issue.

I think i'm losing what your argument is at this point. Could you clarify?

@CyrusNajmabadi
Copy link
Member

@vladd That's ok, there's no one arguing for this (not even me) :)

it just doesn't fit cleanly enough. Not with the language and not with the runtime.

If we could have had all this done for V1, then i would have felt differently. But we're 16 years in, and life is full of compromises :)

@asik
Copy link

asik commented Apr 17, 2016

I'm not understanding the concern with this code. It would stay legal, just as it is today... Could you clarify what the issue would be with this?

The developer probably intended to set each item's X property to 10. If item is a mutable struct (e.g. list is a List<(int X, int Y)>), the information is lost as you're only modifying the function parameter.
Not only that but you have to change the coding style (switch to a for(int i = 0;- style loop) to fix the bug.

This is but one example of how mutable structs cause bugs, and why they're usually avoided. If you didn't spot this (and honestly, I don't expect many people to spot this easily), then it proves my point very well.

This has been a well accepted principle in the community, if you look at top-rated Stackoverflow posts and blogs by people like Eric Lippert, Jon Skeet, Marc Gravel, etc., .NET design guidelines, most the .NET framework class libraries, and it's nothing new; C++ has lots of gotchas that people learn to work around because of the same reason (always passing things by reference, const reference if possible, etc). I'm kind of baffled that we get a new built-in type that's going to be a mutable value type without much concern about the pitfalls we know it'll create. We're all familiar with these and we've known them since even before C# was created. Mutable value types are and always were inherently difficult to reason about.

I'm also amazed by the weakness of the counter-arguments presented so far - that they look like parameter lists (how does that solve anything?), or to suggest that adding more complexity like additional warnings and analyzers is a better idea than simplifying the feature and making these errors impossible in the first place, or that there other somewhat similar pitfalls in the language (how does that justify adding new ones?), or that the question was brought up before and brushed aside by some vague remark by a designer...

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 17, 2016

Not only that but you have to change the coding style (switch to a for(int i = 0;- style loop) to fix the bug.

That won't help if you're actually using a List<(int, int)>. this[int index] on a List<T> does not return a ref T, it returns a T. So no matter how you iterate, you can't assign into your copy and see it updated in the list. Instead, you'd actually have to write something like list[index] = ... in order for any writes to be visible in the list.

This is but one example of how mutable structs cause bugs, and why they're usually avoided. If you didn't spot this (and honestly, I don't expect many people to spot this easily), then it proves my point very well.

It's your claim that this is a bug. With the code out of context like that, i see no bug. :)

You mentioned 'probably' and i could respond with "the dev probably just wanted to tweak their copy of the tuple before using the values later on in the method."

This has been a well accepted principle in the community

I've already mentioned several times that guidelines** are not absolutes. They are general principles which make sense most of the time, but which may not be the right thing all of the time. As a language designer, i prefer us take a pragmatic approach to these sorts of things, versus blindly disregarding options.

In the case discussed here, we know we want tuples to be entities that you can safely pass to others, without having to worry about shared mutable state. We also know we want them to behave very similarly to the existing collections of entities we have today in C# (i.e. parameter lists, etc.). The current implementation gives us that, with very little downside AFAICT.

**
In the case of mutable structs, the guidance is around generating new named structs that are mutable. But that doesn't mean the same guidance holds for a language construct. For one thing, if you have mutable named structs, you now have to convey that information to everyone who uses that API. However, for tuples, we only have to explain the issue once, and now that knowledge can be used everywhere else. in other words, if i have a Frob or a Blap, is that a mutable struct (or even a struct at all)? I don't know, i'd have to go find out. And, my gut might just assume that they're both reference types. So each time you add a mutable struct, you have the problem that there's a good chance that someone won't even realize that's the case. This is very different from tuples. Once you learn it once for tuples, then you know that aspect no matter where you see the type. i.e. if i have Foo((Frob, Blap) t) or (Frob, Blap, int, string, bool) M() then i know these are tuples and i understand how they work. It's not like i'm learning about a new type every time around.

I'm kind of baffled that we get a new built-in type that's going to be a mutable value type without much concern about the pitfalls we know it'll create

I am not convinced that this will create major pitfalls. Or, rephrased, i think the problems being brought up here are being over-exaggerated. I think people will learn how these work immediately. And will move on with their coding. They may get bitten once, and then they'll move on. It's akin to how Nullable has different semantics for a < b || a == b vs a <= b. Is it an issue? Sure. Will some people get hit by it? Sure. Is it a big deal? Not at all. For most people it works as they expect, and if they run into this issue, they learn what's up and move on.

I'm also amazed by the weakness of the counter-arguments presented so far - that they look like parameter lists (how does that solve anything?)

Well, you keep stating that it will be a pitfall for people. I think it would be more of a pitfall if these types didn't behave like the existing language features they look nearly identical to. Now the language feels strange. Like we bolted on a new feature rather than incorporating it into the existing language features.

or to suggest that adding more complexity like additional warnings and analyzers is a better idea than simplifying the feature and making these errors impossible in the first place

This is a strawman argument. The point aobut warnings and analyzers was for code that was strictly doing things that could not be observed (which is, in general, a sign that something is going wrong). We'd still want the analyzer and warning, even if tuples were immutable. i.e. if you had this:

void F() 
{
      var tuple = ...
      ...
      tuple = (foo, bar);
}

We'd still want a warning here as this assignment had no side effects and could never be observed.

Furthermore, the point about analyzers was to warn on code that we could strongly feel was wrong. There's lots more code you can have with mutable tuples that doesn't feel wrong, and indeed feels totally natural. Specifically, any existing code that writes into parameter or local today. That code, if tuple-ized, would feel just as natural, and we would want to make possible to write.

or that there other somewhat similar pitfalls in the language (how does that justify adding new ones?)

Because language design is a pragmatic exercise. There are rarely perfect answers. Indeed, tuples perfectly exemplify that. If we go the way i've discussed, there are the pitfalls you don't like. If we go the way you want, then there are pitfalls in other areas. If there were no pitfalls then we wouldn't need to discuss things. We wouldn't need to attempt to balance a ton of factors. And we could simply crank out language features trivially.

In the discussion here you've strongly taken the approach that the presence of this specific pitfall is anathema to you. I've attempted to point out that this language has always faced pitfalls with nearly any feature added. The goal of the design is to come up with something we think minimized the impact of the pitfalls and maximizes the value in what's being delivered. In the case of this discussion, i think the pitfalls in your proposal vastly outweigh the pitfalls in the proposal as specified currently.

or that the question was brought up before and brushed aside by some vague remark by a designer...

I've provided a lot of information on the thought process going on here. We're very well aware of the thoughts around mutable tuples, and the decision to go that was wasn't done lightly, or in a vacuum. It was discussed in depth. However, what seems to be lost here is that as part of the discussion, it's rare to have people take ideological positions. No one says "i don't want mutable tuples because mutable tuples are bad". Instead, we talk about the issues with mutability and immutability. We talk about hte issue with value and reference types. We talk about the how we want this feature ot feel and how it fits into the existing language. We talk in depth about the sort of code we want ot be able to write. And how we want to integrate that into existing codebases that we work on. And based on that, we decided that the value in going with this approach far outweighed what we saw a very small issue in actual practice.

We aren't flippant about these decisions. In the past 2 weeks or so we've dozens of hours of discussion over incredible minutia around tuples, pattern matching, and existing language features. We hem and we haw and we go round and round as different designers weigh in on their feelings about these choices. And in the end, we can either choose to do:

  1. do nothing. Decide that there is just too many issues to move forward.
  2. do something. Decide that we've found the right balance and that the net value outweighs any net negatives.

I hope this helps you understand where we're coming from. As mentioned already, your feedback has been received. I cannot promise you will get what you want. But i can promise that it will absolutely be considered and we'll continue doing what we can to make what we think are the right choices here.

@asik
Copy link

asik commented Apr 17, 2016

It's your claim that this is a bug. With the code out of context like that, i see no bug. :) You mentioned 'probably' and i could respond with "the dev probably just wanted to tweak their copy of the tuple before using the values later on in the method."

Really. The dev computed a value and stored it inside an arbitrary field of a function argument just to log it. That's an equally likely interpretation of that code.

Well, that's kind of a fantastic thing to say. It's like seeing a potential random AccessViolation and saying the dev intended for the AccessViolation to randomly happen. It's possible, that doesn't mean it's an equally likely explanation.

The code above is almost certainly wrong, and it's but one example of many. If mutable value types are generally considered "evil" it's because they've earned that reputation.

Well, you keep stating that it will be a pitfall for people. I think it would be more of a pitfall if these types didn't behave like the existing language features they look nearly identical to. Now the language feels strange.

I must have missed something, but could you point out in what way tuples would behave less like existing language features if they were immutable? You couldn't re-assign them when used in a pattern-matching argument list? something like
void Func((int a, int b) tuple) // can't re-assign a or b because they're immutable fields

Yeah, it's a bit strange, but it's caught at compile time, and it's not causing any bugs; actually, it's preventing them in many cases. I'm a lot more concerned about pitfalls that'll introduce bugs in my codebase, than pitfalls that'll cause developers to think and fix their code.

If it's just about maintaining that symmetry I really fail to see how it "far outweights" the pitfalls of mutable value types.

As mentioned already, your feedback has been received. I cannot promise you will get what you want. But i can promise that it will absolutely be considered and we'll continue doing what we can to make what we think are the right choices here.

I thank you for taking the time to answer me. Hopefully my fears are unfounded, or I don't understand the tradeoff being made, but so far I am not seeing it.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 17, 2016

It's possible, that doesn't mean it's an equally likely explanation.

Out of context, i really can't say one way or the other.

I don't know why i would ever expect to see code like that period, so i can't use it effectively to determine if this issue is actually bad or not.

You couldn't re-assign them when used in a pattern-matching argument list? something like

Yes. And now we've introduced a new feature that does not play well with the existing features it's intended to complement.

Yeah, it's a bit strange

Yes. And that strangeness outweighs the other things you don't like for me :)

but it's caught at compile time, and it's not causing any bugs

And, at the same time, it will feel highly restrictive. I feel like the paragraphs i wrote about pragmatism and balancing things may have been missed.

If it's just

It's rarely ever "just" about one thing. As mentioned already, there are a lot of considerations going into the tuple language feature. As there is no perfect design that solves all the problems, the goal of the process is to come up with a reasonably pragmatic design that does a good job overall, without feeling beholden to ideological concerns.

This conversation has been an attempt to make you aware of what people are thinking about. You clearly feel very differently. And that's ok. That's been par for the course with effectively every language change we've made since the beginning. Generics/Nullable/Linq/ExtensionMethods/Var/Lambdas/etc. etc. etc. all ended up with huge amounts of disagreement at the their time. And none of which came around without some amount of pragmatic addressing of many different concerns across many different people. In the end, nothing we've made it perfect. But we've seen this approach produce very viable, long lasting, designs that tend to provide a net amount of value that the community appreciates.

If it's just about maintaining that symmetry I really fail to see how it "far outweights" the pitfalls of mutable value types.

I understand that. As mentioned already, everyone has different ways that they weight all these things. You've made your point very clear about what you think the issue is.

At this point, i don't necessarily know what you're trying to accomplish. I've simply put forth information to help you better understand the perspective of those who feel differently from you. The intent was not to get you to start thinking differently. Instead, it was simply to inform. Right now it feels like i'm hearing the same arguments be made for why tuples should be immutable. But, as i've mentioned already, that feedback has been heard and will absolutely be considered. Is there additional information you want to provide for this area? If not, i would recommend we move on because the conversation here appears to have gone entirely circular at this point...

@alrz
Copy link
Contributor

alrz commented Apr 17, 2016

the conversation here appears to have gone entirely circular

Better be a compile-time error.

@asik
Copy link

asik commented Apr 17, 2016

Is there additional information you want to provide for this area?

Perhaps it's not been pointed out yet that if tuples are mutable, they will be different in that respect from two other similar constructs: records and anonymous types.

This means that I cannot easily refactor code written using tuples to use records: a common scenario in F# (tuples are often used for prototyping and then converted to records where it makes sense for maintainability and readability).

This also means that if pattern-matching in argument lists is ever extended to records and other types:

void Func({ A = a; B = b} record) // hypothetical F#-like syntax for record deconstruction

Then those can't be reassigned, and now the value of having tuples behave like that seems lessened as it becomes inconsistent with similar features.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 17, 2016

other similar constructs: records

From my understanding, there is no requirement that records be immutable in our designs for that language feature.

They will be immutable by default when you pick the minimal syntax, but you can opt out of that if you want. For example, you could write your record as:

public sealed class Student(string Name, decimal Gpa)
{
     public string Name { get; set; }
}

It's also worth noting that records are different (if you create the reference type version of them). That's because now you would have shared mutable state, and that's something we do generally feel like we want to avoid.

The shared mutable state issue is not there with tuples as we've designed them because you always pass around a copy.

So, in essence, we have some things we care about avoiding (i.e. shared mutable state). And we're picking our defaults to align with that appropriately depending on if you have a reference type or a value type. For a reference type to avoid shared mutable state, it needs to be immutable. For a value type, you don't need to do anything as you can't share state to begin with :)

and anonymous types.

Anonymous types can be shared. As they are reference types, then being mutable would be highly problematic. Note: tuples do provide a reasonable migration path forward from anonymous types. As anonymous types are immutable, a mutable type is no problem to move to.

Then those can't be reassigned, and now the value of having tuples behave like that seems lessened.

That's several compounded assumptions. For one, i have no idea why we'd assume they could not be reassigned.

This means that I cannot easily refactor code written using tuples to use records.

I don't see any reason why you would have any issue here. You could strictly move from the struct-based tuple approach to the struct-based record approach. Then you would pick up no shared mutable state for free. If you went the reference based record approach, you'd not have to decide what what sort of behavior you wanted.

By default, our tuples will provide the same sort of behavior you get from a list of parameters. As you move further and further away from that, you have the knobs to decide what you want to do and how much you want to refactor.

@asik
Copy link

asik commented Apr 17, 2016

That's several compounded assumptions. For one, i have no idea why we'd assume they could not be reassigned.

For the same reason that immutable tuples would prevent it; each apparent argument is actually a reference to a readonly field (or get-only property in the case of records). I wasn't aware records could be mutable, but we can still suppose most will be immutable (the syntax pushes for that). And then the aforementioned symmetry with parameter lists cannot be maintained.

@HaloFour
Copy link

The symmetry is between tuples and argument lists, not records and argument lists. If you want to prototype with tuples and then refactor to records that's on you. Expecting C# to mirror it's features after F# specifically because you have a development work flow in mind is not reasonable.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 18, 2016

I wasn't aware records could be mutable

We're currently viewing 'records' as really just a list of features that allows you to concisely define types. But they would be a very weak feature if they they then didn't allow flexibility for when you needed the type to behave differently.

Here's the relevant bit from the january design meeting:

If you want to supercede default behavior, you can give it a body and do that explicitly For instance, you could make X mutable:

class Point(int X, int Y)
{
    public int X { get; set; } = X;
}

Things of it like how things are today. You can have a class definition, usually representing some very mutable thing, and you can put a lot of work into it to make it appear more immutable. Records allow us to go from things in the opposite direction. You write very little, and you have a useful immutable data type. But if you can put more work in to get it to be more like the normal class case if you want to. In the end, they're effectively the same thing. They just have different syntax to allow you to decide where on the spectrum you want to start with.

Of course, all things can change in the future, and we're still trying to work out exactly what we want the design to be for all of these things.

All that said, i'm not sure what this has to do with tuples. in the context of changing between these different types of types, i don't really see what the problem will be. You should be able to move from a tuple to a record, just as you could to a class or a struct. Moving in the other direction may or may not be possible depending on how you've designed your code.

That said, the goal is to put these into the language so you have a spectrum of capabilities and power. Tuples fall fairly low on that spectrum. At the end of the day, they're just really an ephemerally named collection of values. They don't have methods on them. There's no inheritance with them. etc. etc. If that's an appropriate language feature for hte problem you're currently trying to solve? Great! If not, it will hopefully go in the developers toolbelt along with all the rest of the language features :)

@gafter
Copy link
Member

gafter commented Apr 25, 2016

The design notes have been archived at https://github.com/dotnet/roslyn/blob/future/docs/designNotes/2016-04-06%20C%23%20Design%20Meeting.md but discussion can continue here.

@Opiumtm
Copy link

Opiumtm commented Oct 12, 2016

Empty struct ValueTuple name isn't intuitive. It is better to name "nuple" as Unit or Nothing indeed.

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