Hacker News new | comments | ask | show | jobs | submit login
Tuples goodness in .NET 4.7.1 (tabsoverspaces.com)
74 points by cincura_net 10 months ago | hide | past | web | favorite | 52 comments

At risk of embarassing myself, I've yet in my work to come across a good use case for tuples. I always end up either using a dictionary or list<type>. Can someone outline a solid use case for tuples? I usually only hear something like "dynamic types". In thinking out loud, I guess maybe it could be handy in API writing, but I would love to learn a new reason.

I'll use them for data passing within a class. The calling method needs multiple values of differing types and I can't personally justify creating a class just to encapsulate those values for a call or two. So I tuple. But that tuple stays within that class and is never exposed or stored.

Good call on the encapsulation. I've encountered the occasional public API with methods that require some type like a Tuple<int, int, string>, and got pretty peeved. And go ahead and name the argument "values" or something similar, for good measure.

The new tuples do allow names, which can be a nice half way house so you can do something like

(bool success, string errormessage, foo addedItem)

as a return type. A big step up from Tuple<bool, string, foo>!

Multiple return values. Using out vars is so clunky to me (and I've been in .Net for about 10 years now).

instead of "bool TryGetSomething(int param, out MyType outvar)", I rather prefer (bool, MyType) TryGetSomething(int param)

With C# 7 out vars are IMO actually now much more ergonomic to use than tuple returns.

  if (TryGetSomething(int param, out var value)) {
    // use it here
If you returned a tuple you'd have to destructure both variables, and have a separate line for the if and the declaration.

Multiple return values is useful in a few cases, like the TryDoSomething pattern with a bool and a value in the success case, and maybe the "Result or error" pattern.

Past that, the best tool in an OO language for returning multiple different values is to return a named class created for the task.

For the "TryDoSomething" use case, I'd much rather have the function return a structured generic type. Something similar to Scala's Try[T]. (Is there an equivalent in F# s standard libraries? I can't remember offhand.)

It ends up being more readable. Safer, too, since, unlike for tuples, you can refuse to give up a result when the operation failed. It's also more ergonomic to use, since you can make it mappable. That way you can chain together a bunch of things that might fail, and pass along any errors, without having to stick a bunch of extra if-statements into your code.

Agreed. I have seen c# methods that e.g. try and parse to an int, and return int? - the Nullable generic wrapper, like a rust Option type. You use the null to mean "not parsed". That works fine for small internal methods.

Yeah, though, one annoyance with Nullable<T> in C# is that T must be a value type. That gets obnoxious when you want to program generically against it, because, e.g., you can't do have a method like

  TValue? TryGetValue<TKey, TValue>(TKey key)
and have it work with any possible TKey. Unless they've fixed this in the past couple years, you get stuck writing your own option type to be able to also work with reference types, which is always fun to try and get past code review, or you fall back on the out parameters idiom, which is not composable.

> Yeah, though, one annoyance with Nullable<T> in C# is that T must be a value type.

Unnecessary because class types already allow null. I would not be surprised if a method e.g. public Customer TryFindCustomer(int id); returns null if it can't find the customer.

Of course, if you want to return either a customer or an error details, then you need another type or generic wrapper.

Not strictly necessary, no. But the example I wrote was getting at was meant to point out that this bifurcation can make it harder to compose generic types. It's not really an ideal design decision for modern C# so much as a hold-over from the language's early years, when idiomatic usage favored a much more imperative style of programming.

Possibly a Result in F#? Is Scala Try the equivalent of an Either monad which automatically catches underlying exceptions from Java interop?

To an approximation it's that. It also makes for somewhat more readable code since the properties are "result" and "error" rather than "left" and "right".

I've never liked to see much use of Either because it has the same readability issues as tuples.

I don't see how the tuple is better in this case since the first thing you need to do is check the flag to see if the value is valid. The tuple version then needs to be split over two lines instead of a single line like

     if(TryGetSomething(3, out MyType outvar)) { ... }

out vars are nice because you don't have to change the return type of the method you are editing. Tuples are kind of a drastic change that you can only easily undertake on initial design.

Also, for your example,

Option<MyType> tryGetSomething(int param) is probably better than either.

Database query results.

Any query beyond the most trivial has a schema that depends on the query, and does not map to any entity other than the types of the expressions in the select clause. Tuples, combined with a nice API (I'm more familiar with jOOQ than LINQ these days, but it should apply to LINQ too) mean you preserve type safety when working with your query results.

List<Type> or similar means you're creating a lot of types in a large project.

Specifically you're making a ton of Dtos or similar objects just to bundle values together and transfer them between layers. You can use anonymous objects, but then the return type is simply "object", which isn't descriptive enough. Plus anonymous objects are flagged to not cross assembly boundaries without wild hacks.

Tuples work in situations where you'd otherwise use a small Dto, which is then combined with a larger Dto further up the food chain. And since it is self-descriptive you're not losing much clarity over the small Dto.

It's been a while since I've used C#, but I used to use tuples when I wanted simple composite-key dictionaries -- typically grouping things up for batch processing, etc. Before LINQ's convenient multiple-key GroupBy and other stuff, you could emulate with Dictionary<Tuple<T1,T2>,T3>.

Tuples are useful when the types are different, in that case a dictionary or list doesn't work since a list(dog, fish) will be of type list<animal>, while a tuple (dog, fish) will be Tuple2<Dog, Fish>.

Could be as simple as a query. (Value, TimeTaken) or something along those lines.

We probably won't switch to 4.7 at work for another year or so but this is probably the feature people on the floor are most excited about.

So does this mean the tuple preserves the type of its members unlike an object[]? What's a situation where you would want to take a collection of objects that don't even share a common interface? (Not bashing tuples, very curious about the benefits of this.)

Edit: I think I understand tuples when you're using them as return types like (MyClass class, Exception e), which can already be done with Tuples just without the nice syntax. But I'm confused about generically taking or returning an ITuple you know nothing about.

You don't have to use .NET 4.7 to get that feature. What you can do is to add a reference to System.ValueTuple NuGet package. Once added, value tuples are there to bring the goodies to your code and business.

Be careful, System.ValueTuple has a compiler requirement too.

So you are correct to say you don't need .Net 4.7, but you DO need a C# 7 compiler to use System.ValueTuple with .Net 4.5/4.6. We tried to use System.ValueTuple and it would work on VS 2017 but fails to build on VS 2015 due to compiler complaints.

We resolved this by adding the NuGet package "Microsoft.Net.Compilers" to the project. This results in a C# 7 compiler traveling with the solution, and no matter what Visual Studio/installed compiler you have it works.

This won't be a problem in the future as most project templates in Visual Studio come with Microsoft.Net.Compilers preinstalled. Only impacts older projects (such as those targeting .Net 4.5/4.6, etc).

The best usage I've seen for them so far is around intermediary LINQ query results. Usually you can use anonymous types, but sometimes you can't. It's a lot easier using tuples than having to add a little POCO class for something you don't really care about, or resorting to something ugly like the old Tuple type.

> What's a situation where you would want to take a collection of objects that don't even share a common interface?

I found myself in a situation exactly like this a while back.

I was given the job of figuring out how to validate data in various stages of an ETL pipeline. I ended up writing a Python program to do the work in a fairly configurable manner.

The key to this working was the fact that tuples in Python can be used as keys for a hash table and are comparable. The program would run different queries in different databases, split the results out into key and value tuples, use the key to store the value tuples in a hash table, and finally do some comparisons.

Since it was all driven by configuration, the code that did the comparison across couldn't know in advance what the data types were or how many columns were involved.

I've used tuples in the past when a method could fail and I want the error code back or I want the thing.


(string errorCode, Quax result) TryGetAQuax();

Oh, to have Either / Discrimated Unions in C# :(

We also can't use this at work, we are still having to target 4.5.1 :(

> Oh, to have Either / Discrimated Unions in C# :(

Here you go: https://sourceforge.net/p/sasa/code/ci/dotnet-standard/tree/...

You can use it like:

    Either<string, int> x = 99;
    if (x.TryCase1(out var s))
    else if (x.TryCase2(out var i))
Only up to 4 cases are included because beyond that, I think you should define a custom type.

Although I should note that C# now supports special casting via the 'is' operator, so you can now just do this:

    object x = 99;
    if (x is string s))
        ... do something with 's'
    else if (x is int i))
        ... do something with 'i'

There's also the OneOf NuGet package [1] which I've put to good use in a number of projects. Using `object` and `is` means you can't enforce what types get passed in, nor can you ensure that all possible types actually get handled.

[1]: https://github.com/mcintyre321/OneOf/

I can never justify the use of delegates for pattern matching purposes. It's just way too much overhead, and so would discourage the use of these abstractions.

> when a method could fail and I want the error code back or I want the thing

For this use case, I mostly use a Result<T> with .Status, .Messages, .Exception, .Data etc properties.

You can :) (Especially with the pattern matching feature) https://github.com/louthy/language-ext

Could you use the System.ValueTuple NuGet package?

Requires 4.6.1

Also works with .NET 4.5. System.ValueTuple package has .NET Standard 1.0 target which can be effortlessly used by a .NET 4.5 assembly.

I hope someone could describe some good scenario. Maybe some low level data exchanges. Database connectors, interop with dynamic languages? Interfacing with hardware drivers? Piping some third party datasource to json where you don't care about types and number of arguments in backend?

Note that using the interface ITuple instead of the tuple types directly will incur boxing (memory allocation for reference type instance).

How is this feature implemented, what is the complexity of access via index? Is it O(1)? Also, adding interfaces onto struct and referencing them by interface causes boxing, right? If so, performance benefit of valuetuple is erased.

It is implemented with a recursive call (if the tuple contains tuples), otherwise, it's a hardcoded constant.

> boxing

You can create a generic function that accepts T where T: ITuple to eliminate this cost.

The base interface for tuples is useful, that's how I did tuples in my Sasa library years ago.

Can't say I see the need to index a tuple dynamically with an integer, or to access a tuple as an object[]. What's the use case for this?

A lot of JavaScript color manipulation libraries use arrays to represent a color. Like c[0] = R, c[1] = G, c[2] = B.

.NET ports of such libraries can be hugely improved by using value tuples for color representation, while keeping the original array-based semantics.

But why keep the original array-based semantics if you're using a tuple?

In fact, I'm not even sure why you'd use a ValueTuple for that at all, because it's not a tuple but more of a vector type because the elements are all of the same type. You'd probably want a more meaningful domain-specific type.

It's nice to finally have actual syntactic sugar for tuples.

Now if only we had Rust-style destructuring.

EDIT: I think we do. This makes things a lot easier.

Would you mind posting a link or an example of the feature in C#/Rust?

You can see it in the official announcement about C# 7: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csh... (scroll down to the part about the Deconstruct() method)

Thanks. This does look very nice. How does the runtime know if you can “Deconstruct” an object? Is there an interface that every Object inherits? Or is it implicit based on whether the object implements the appropriate method?

If the latter, C# is gaining some very golang-like attributes. I also noticed the special ‘Range’ keyword and Discarded assignments with underscore. I think this can only be a good thing. C# and go are among my favorite languages to use and the more good ideas they adopt from the other, the better.

It's implicit based on the appropriate method overload being present. In fact that's the way foreach loops and LINQ already work in C#, you don't actually need to implement IEnumerable or IQueryable to use them.


Ah, that’s interesting. I always just assumed everything I was using inherited IEnumerable somewhere way down the line. Thought it’s possible that was the case too. It’s been a few years for me and I didn’t do much outside of standard library / entity framework.

> How does the runtime know if you can “Deconstruct” an object? It implicit based on whether the object implements the appropriate method?

Based on what I have read below, it's implicit; there's very little that the runtime has to know - it happens at compile time, with the compiler looking for a method with the name "Deconstruct" and the right type signature, and plugging that in during the compile.

The compiler already does a lot of heavy lifting codegen for language features like async / await



I assume it is the latter: since the creation of the tuple is happening at compile-time, it's easy and effectively free to just quickly check with reflection if there is a Deconstruct() method.

in Rust:

let (a, b) = (5, 6);

will de-structure the tuple, and assign 5 to a, and 6 to b. This works more generally:

  fn foo(tuple: (i32, i32)) {
      let (a, b) = tuple;
or even

  fn foo((a, b): (i32, i32)) {

Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact