csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

zero- and one-element tuples

Open gafter opened this issue 6 years ago • 41 comments

@gafter commented on Fri Jan 06 2017

To best align positional pattern-matching (which won't restrict the number of values being deconstructed) with tuples, please support zero-element and one-element tuple types and tuple expressions.


@AdamSpeight2008 commented on Fri Jan 06 2017

Doesn't the current tuple parser require a minimum of 2 values?


@alrz commented on Fri Jan 06 2017

@AdamSpeight2008 Yes.


@bbarry commented on Fri Jan 06 2017

Why not align the other way, don't recognize deconstruction syntax with fewer than 2 elements?


@HaloFour commented on Fri Jan 06 2017

@gafter

I assume that this will be a post-C# 7.0 consideration?

@bbarry

Without support for conditional deconstruction of zero/one values then patterns like None() and Some(T) would be impossible.


@orthoxerox commented on Sat Jan 07 2017

@bbarry Single-value tuples are necessary for primitive wrapper types.


@dsaf commented on Mon Jan 09 2017

Would leveraging it for naming primitive return values be considered an abuse?

(int id) SaveSomethingToDb(Something smth);

instead of

int SaveSomethingToDbAndReturnId(Something smth);

@HaloFour commented on Mon Jan 09 2017

I like how Swift handles "womples", they're effectively just the scalar value itself. So an Int32 is an (Int32) is an ((Int32)). This makes sense as 1, (1) and ((1)) are all the same value. Swift does not allow for naming the element of a "womple", though, which kind of makes sense.


@Richiban commented on Tue Jan 31 2017

Will the zero-tuple be explicitly referred to as unit anywhere?

I'd love to see some support for this in the future, such as being able to use Func<int, ()> instead of Action<int>, for example.


@alrz commented on Tue Jan 31 2017

I think the type (and literal) for unit could be () if it wasn't ambigious, but using it in place of void probably requires language support so you dont need to explicitly return ().


@jcouv commented on Tue Sep 05 2017

@gafter Is this tracking a compiler/API change or a language feature? If the latter, could you close or move to csharplang?

gafter avatar Sep 06 '17 17:09 gafter

What's the purpose of a zero-element tuple?

yaakov-h avatar Sep 06 '17 19:09 yaakov-h

Purpose.. probably to satisfy a desire for a unit type/value that some functional languages have (as opposed to 'void' that C#/.net has), and some language's common convention of using empty collections or zero-valued tuples to represent nothing, aka unit.

mattwar avatar Sep 06 '17 20:09 mattwar

There might also be a need to support them conceptually to permit deconstruction of zero and one element patterns, e.g. Some(T) and None().

HaloFour avatar Sep 06 '17 23:09 HaloFour

@yaakov-h,

As @mattwar says, () is another way of saying unit, ie no value.

However, I feel it would be a bad idea to introduce yet another type (unit) to C# to represent that (no) value, as we already have two other ways of indicating no value: null and void. I'd far prefer to see the language support the idea of void expressions (eg, see #135) and to have the syntax, (), be a way of expressing void in such expressions:

void Foo(bool b) => b ? Console.WriteLine("True!") : ();

Having said that. I'm not really convinced that () adds anything that couldn't be covered by just using void:

void Foo(bool b) => b ? Console.WriteLine("True!") : void;

but I may be missing some use-case for it.

DavidArno avatar Sep 07 '17 08:09 DavidArno

I'm thinking that they are, wanting to be able to treat Tuple and ValueTuple as a pseudo-base class.

tuple result = SomeFunction();
switch( result )
{
  case Is (): /* roughly MayBe.None */
  case Is (var A): /* roughly MayBase.Some<T> */
  case Is (var A, var B):
}

with SomeFunction roughly being

() xor (T) SomeFunction()
{
  If( condition )
  {
    return ( Value );
  }
  else
  {
   return ();
  }
}

AdamSpeight2008 avatar Sep 07 '17 14:09 AdamSpeight2008

If you have:

void Foo((int) x);

How would you call it? Would it implicitly convert Foo(1)? You can't use any number of parens like Foo((1)) to disambiguate it.

scalablecory avatar Sep 07 '17 23:09 scalablecory

@scalablecory

I like how Apple Swift does it. A single-element tuple (womple) is just an alternative syntax for a scalar value. An (Int) is just an Int, which kind of makes sense syntactically as (123) is really just 123. But one of the side-effects is that unlike tuples of two or more elements, you can't name that single element.

HaloFour avatar Sep 08 '17 00:09 HaloFour

@HaloFour even if it does boil down to int I think feature parity (naming deconstruction etc.) could all be accomplished with the help of attributes.

scalablecory avatar Sep 08 '17 00:09 scalablecory

But one of the side-effects is that unlike tuples of two or more elements, you can't name that single element.

I consider this a feature though. 😆

jnm2 avatar Sep 08 '17 01:09 jnm2

but I may be missing some use-case for it.

A real unit type would be very useful for generic types. Take a look on tasks. We have there two types in C#, one is Task the other Task<T>. With a language featured unit type from the beginning, Task would have been simply Task<()> or Task<void>.

For tasks this is already to late. But think of async enumerables. If IAsyncEnumerable<T> is the sequential counterpart of Task<T>, what is the counterpart for Task? I doubt there will be a non-generic IAsyncEnumerable, because it's not worth the effort. On the other hand we have to life then with the lack of symmetry.

quinmars avatar Sep 08 '17 06:09 quinmars

@quinmars,

I agree that a real unit type would be useful. If it's feasible though, then I'd like to see void become that unit type, so that I could do Task<void> or IAsyncEnumerable<void>.

Of course, it may not be practicable. In that case, I think the next best solution is a Unit type (and unit keyword alias), but that has similar semantics to void, especially with regards to return:

unit Foo() 
{
    Console.WriteLine("foo");
} // no return needed as unit has no value to return

DavidArno avatar Sep 08 '17 08:09 DavidArno

@DavidArno I think void should be the alias for the unit type (). It must be an alias for a meaningful unit type, rather than a shorthand (longhand?) for Task so that List<void> compiles.

YairHalberstadt avatar Sep 15 '17 10:09 YairHalberstadt

I doubt there will be a non-generic IAsyncEnumerable, because it's not worth the effort.

But let's assume we're willing to put any amount of effort into it. What would it do, exactly? Lack a Current property?

jnm2 avatar Sep 15 '17 11:09 jnm2

In that case, I think the next best solution is a Unit type (and unit keyword alias), but that has similar semantics to void, especially with regards to return:

So basically, exactly what I described here.

Joe4evr avatar Sep 15 '17 13:09 Joe4evr

@YairHalberstadt

I think void should be the alias for the unit type ().

void already means something quite different in C#, and is valid syntax in some scenarios where it would collide with any unit type, e.g. typeof(void).

HaloFour avatar Sep 15 '17 13:09 HaloFour

In that case, I think the next best solution is a Unit type (and unit keyword alias), but that has similar semantics to void, especially with regards to return:

So basically, exactly what I described here.

It appears to be the best what we can get. And I like it. The following feels very natrual:

async IAsyncEnumerable<unit> ToAsyncEnumerable(IEnumerable<Task> list)
{
    foreach (var t in list)
    {
        await t;
        yield return;
    }
} 

and:

// T can be unit!
async IAsyncEnumerable<T> ToAsyncEnumerable<T>(IEnumerable<Task<T>> list)
{
    foreach (var t in list)
    {
        yield return await t;
    }
} 

I doubt there will be a non-generic IAsyncEnumerable, because it's not worth the effort.

But let's assume we're willing to put any amount of effort into it. What would it do, exactly? Lack a Current property?

Probably.

quinmars avatar Sep 19 '17 21:09 quinmars

Non-generic IEnumerable is an enumerable of result objects. Making IAsyncEnumerable an enumerable of non-results seems strange and asymmetric.

yaakov-h avatar Sep 19 '17 22:09 yaakov-h

If there is no result being produced for each MoveNext, why would anyone want to call MoveNextAsync in a loop until it returns false versus just awaiting a non-generic Task?

jnm2 avatar Sep 19 '17 22:09 jnm2

@jm2, an IAsyncEnumeration<unit> is not a just a collection of the same useless value, it has a second dimension: the timing.

Let's say you want to write an email to a list of recipients via Task SendEmailAsync(string r), but only for 5 seconds. You could write:

var lastOrder = DateTime.Now + TimeSpan.FromSeconds(5);
var orders = recipients
    .Select(r => SendEmailAsync(r)) // due to lazy evaluation the mails are not sended now
    .ToAsyncEnumerable() // The (extension) method in my last post
    .TakeWhile(_ => DateTime.Now < lastOrder);

foreach await (var _ in orders)
{
    // We can write this line after every sended email
    // not just when all emails are sended.
    Console.WriteLine($"Email sended at {DateTime.Now}");
}

quinmars avatar Sep 20 '17 06:09 quinmars

@jm2 and @yaakov-h, don't get me wrong. I do not think that it is reasonable to add a non-generic IAsyncEnumerable. I just wanted to show that there is a symmetry gap, which an unit type could fill.

quinmars avatar Sep 20 '17 06:09 quinmars

I don't think one-element tuples should be in any way "equivalent" to the value itself. If you have any other tuple, it's just a container type and to get the actual value, you have to dot into it (and can even give it a custom name). The logical extension of this would be to do the same for a one-tuple. Why should it be any different?

Neme12 avatar Jan 11 '18 15:01 Neme12

@Neme12

Tuples are only somewhat containers in the C# language. You have to dot into them out of necessity, but in some cases the language does (or will) treat them just as separate values and ignore that container. For example, the compiler already completely skips the container if you immediately deconstruct a tuple literal. This is despite the fact that the ValueTuple<...> implementation could technically do something custom in its constructor. There is also a championed proposal to have the compiler allow equality comparisons between tuples by emitting direct comparisons between the tuple elements, again skipping any potential equality operator implementation on ValueTuple<...>:

(int, int) GetPoint(int x, int y) => (x, y);
(float, float) GetPointF(float x, float y) => (x, y);

var x = GetPoint(1, 2);
var y = GetPointF(1.0f, 2.0f);

if (x == y) { ... }
// converted to
if (x.Item1 == y.Item1 && x.Item2 == y.Item2) { ... }

IIRC, in math a "womple" is just the single value. In various programming languages (e.g. Swift) that's also the case. There are probably advantages and disadvantages to both approaches. I'm not particularly vested in either, but I'd like to hear scenarios where womples are actually useful/necessary.

HaloFour avatar Jan 11 '18 16:01 HaloFour

@HaloFour My point is you still have to deconstruct it to get the value. I don't see why the pattern shouldn't continue:

var a = (item1: 1, item2: 2, item3: 3).item1;  // OK
var b = (item1: 1, item2: 2).item1;    // OK
var c = (item1: 1).item1;    // error?

That would really be unexpected for me for the one-tuple to behave completely differently.

I can't think of any usage for a 1-tuple, I'm more interested in 0-tuples, and it would definitely be weird to have 0 and not 1. I think they should be allowed even if it's just for consistency.

Also, if you do go all the way down to zero:

var a = (item1: 1, item2: 2, item3: 3);  // type == ValueTuple
var b = (item1: 1, item2: 2);    // type == ValueTuple
var c = (item1: 1);    // type == int ?
var d = ();    // type == ValueTuple

In your example, a one dimensional point equality would just be translated to x.Item1 == y.Item1

IIRC, in math a "womple" is just the single value.

C# is not math (and I'm quite happy about that) 😄

Neme12 avatar Jan 11 '18 16:01 Neme12

I'd like var c = (item1: 1); // type == ValueTuple<int> to be available.

I've found a way of serializing the tuple property names, and it's quite annoying not to be able to use "single value + name" tuples.

mcintyre321 avatar Apr 13 '18 14:04 mcintyre321

Func<(T1 x1, T2 x2)>
Func<(T1 x1)>
Func<()> // I really want this
...

Task<(T1 x1, T2 x2)>
Task<(T1 x1)>
Task<()> // I really want this
...

ufcpp avatar Apr 13 '18 14:04 ufcpp

Does anyone know if this will be considered at any point? I keep having to use a dummy parameter in certain situations e.g. class SomeCommand : Command<(string someArgument, object _)> { ... } ...

mcintyre321 avatar Nov 20 '18 10:11 mcintyre321

@mcintyre321 Can you expand on your scenario? Why not just do class SomeCommand : Command<string> ...?

jcouv avatar Nov 20 '18 18:11 jcouv

I want the value to be named, not just for sugar, but for serialisation as well (I have some code that will read the name attributes).

mcintyre321 avatar Nov 20 '18 19:11 mcintyre321

Sounds like a bad case of Round Peg, Square Hole. If you want named values, especially for the purpose of serialization, it's much more appropriate to use a Dictionary<string, Command>.

Joe4evr avatar Nov 20 '18 22:11 Joe4evr

I probably haven't explained my use cases sufficiently. I find there are LOTS of situations where I would like to create a class where one would use a (potentially deeply nested) ValueTuple as a generic parameter, e.g. in a MessageHandler<TIn, TOut> which works fine, except when I have single valued items.

or public class EngineTemperature : ValueOf<int> { } vs public class EngineTemperature : ValueOf<(int Celsius)> { }. I find the latter more helpful, but it won't compile. (NB I'm using my ValueOf library here, but this will be a problem with Record Types once they are part of the language).

Maybe these use cases aren't that common (yet - I think the Record Type use case is very desirable as you will be able to define structures for API messages very concisely), but the fact that single item ValueTuples have different rule (e.g. you have to use the class explicitly (as in public ValueTuple<int> GetSomething(){...}, rather than being able to do public (int) GetSomething(){...) complicates the language, and increases code churn when refactoring.

mcintyre321 avatar Nov 21 '18 10:11 mcintyre321