Why I dislike tuple return types

Tuples are great additions to C#. They're simple immutable structures made of a small number of members organized in a specific order. For example, a point on a plane could be represented as a pair of X and Y coordinates, or a person's name could be represented as a title, first, middle and last names. If the types are that simple, and have no custom logic associated with them, using a tuple looks like a lightweight and simple implementation that requires a lot less effort than a simple class or struct. Dynamic and functional languages have long enjoyed tuples, of course, but I like how the C# implementation manages to stay true to the core tenets of the language, in particular type safety.

However, I personally cringe a little every time I see a public API returning a tuple. I really, really dislike them as return types. Let me explain why.

As a user of a method, you will use what you know of the return type, and use the data contained therein according to your needs. There's a whole category of backwards-compatible changes to a class, such as adding a new member, that are trivial with classes and require no change or even recompilation of client code, that become very difficult with tuples.

This comes from the fact that with tuples, you need to copy the very definition of the type everywhere you use it, whereas with a class, you only use its name as a reference to the type. Almost no refactoring is possible with tuples without finding all of the code in the world that uses your tuple return type, and modifying it as well. This is at best impractical. Tooling can't even help you much beyond CTRL+SHIFT+F and finding all references to the method. I know from experience that tracking down those types in a large codebase can be far from trivial.

It's actually less effort in the end to write a simple class. I have a lot of hope for records (vote) as a better option to use wherever you need lightweight types, but in the meantime, I prefer in most cases to write a simple class rather than to return a tuple.

There's also the fact that a named type can in most cases better convey the semantics and intentions of your code than a tuple, which is pure structure. Finally, the temptation to write Cthulhuesque abominations such as IEnumerable<(string, IDictionary<string, (string, string)>)> exists. When you have to put the name of your method on a different line than its return type, you know you've gone too far.

So at this point, you may ask, what do I still think tuples are good for? That is a fair question... I have several answers to that.

First, I don't think tuples should always be avoided as return types. When the semantics of a method are to manipulate or produce a tuple, they are more than acceptable, in fact nothing else makes sense:

I think tuples are also fantastic at simplifying code that produces and processes intermediary results. I particularly like to use them as return types of Lambdas in LINQ expressions:

This totally non-optimized, allocation-happy and non parameter validating piece of code conveys the idea: it uses a simple tuple as an intermediary structure between the Zip call and the SelectMany. The tuple brings real value here, as doing the same thing without it would be considerably more tedious and less expressive (let's put aside the existence of a Pair class that could be used in this simple case, the general point remains). The proximity of the code that produces the tuple to the code that consumes it, associated with a tight scope means none of the objections I have to using tuples as public API return types apply here. It's pure goodness.

Another very compelling feature of C# tuples is deconstruction, and I wish we also had it for anonymous objects like JavaScript does. Deconstruction allows you to directly assign values inside of a tuple that only lives as part of that assignment. It is a very convenient way to access and use the components in the tuple:

This is really lovely, and the good news is that tuples are not the only types this works on: you can add type-safe deconstructing capabilities to your own classes by implementing something like this:

The GetMiddlePoint deconstruction sample above will work exactly the same whether the method returns a (double, double) tuple or an instance of a Point class that implements a Deconstruct method. This is a great way to refactor a tuple-returning method to return an instance of a class instead, while keeping client code untouched (recompiling may be necessary). To me this is the best of both worlds: a deconstruct-aware class remains easy to refactor cleanly, and can still present the greatly expressive deconstruction capabilities of a tuple.

6 Comments

  • Nice food for though, although I wonder if the problem of "copying the very definition of the type everywhere you use it" could be solved though better type inference. I've also been pondering how types can replace the needs of classes in cases like this. For example returning (string, string) is objectively terrible but returning (StudentId, TeacherId) is exact and unlikely to break or change.

  • Thanks for the comment, Daniel. Well, if you're going to use the niceties of tuples, such as deconstruction, you're going to have to copy that structure around. Even with named members, the arity is still fixed by that definition and can't be easily changed. I guess you could try to get people to use var as much as possible and be sort of fine, but as an API designer, that's not a very nice thing to do because you'd be prescribing how your users should be formally using it.

  • Probably the nicest feature of tuples (when used internally to a method), is that if you compare:
    * Simple Type like: class MyClass {string FirstName; string Surname;}
    * Anonymous Type
    * Tuple.Create (original Tuple API)
    * C# Tuples (string, string)

    The C# tuples are the only one above that will return TRUE for these three statements (at least without implementing additional overrides in the simple type), where x and y are new instances that contain exactly the same data:
    ```
    x == y
    x.Equals(y)
    new List(){x}.Contains(y)
    ```
    This, due to the change made in C# 7.3 and above, makes for nice code:
    ```
    if ( (firstName, surname, age)==("David","Taylor", 40) ) { \\blah }
    \\ Or if you have an existing type you might write
    if ( (item.firstName, item.surname, item.age)==("David", "Taylor", 40) ) { \\blah }
    ```
    That is really nice. But you point about returning a tuple is well taken. I had not really considered that someone would need to write (x,y,_) = ... in client code if an extra parameter was added to the return type (that the return type wanted to ignore). As you note in your comment, you could attempt to write var mytuple= ... to avoid that issue...but are not always in control of client code. Thanks for your comments.

  • That's a great point. I'd add hash code generation to that list. If you want to override the hash code for a simple immutable class you built, the easiest is to do something like `public override int GetHashCode() => (SomeProperty, SomeOtherProperty).GetHashCode`. There's also the new hash code utility class that can help, but the tuple solution is very expressive and efficient and I prefer it for simple cases.

  • Where have you been last two years?

  • :D @dbj lol. Yeah, sorry. Busy is what I've been. I've been wanting to blog a lot more. I didn't for a couple of reasons. First, nowadays, I tend to prefer to spend time writing code than writing posts. That's very egotistical of my part. Second, more and more, I see my opinions, stuff that I would have easily blogged about in the past, to be very debatable. So many time over the past two years, I've been wanting to write a post, wrote a draft, and then decided that I actually had less of an interesting point than I thought I did. So one thing I may do is write posts that are a little more meta in that they would describe that path of self-doubt. Let me know if that's something you'd be interested in reading.

    This post was different in that I thought there was a clear and unambiguous conclusion: I still consider the propagation of the type definition to client code to be a serious objection to using tuples as public API return types, so I think this post is justified. I also haven't seen that expressed in this way elsewhere, so I believe it's reasonably novel.

Comments have been disabled for this content.