Why use tuples instead of objects?

Viewed 11948

The codebase where I work has an object called Pair where A and B are the types of the first and second values in the Pair. I find this object to be offensive, because it gets used instead of an object with clearly named members. So I find this:

List<Pair<Integer, Integer>> productIds = blah();
// snip many lines and method calls

void doSomething(Pair<Integer, Integer> id) {
  Integer productId = id.first();
  Integer quantity = id.second();
}

Instead of

class ProductsOrdered {
  int productId;
  int quantityOrdered;
  // accessor methods, etc
}

List<ProductsOrderded> productsOrdered = blah();

Many other uses of the Pair in the codebase are similarly bad-smelling.

I Googled tuples and they seem to be often misunderstood or used in dubious ways. Is there a convincing argument for or against their use? I can appreciate not wanting to create huge class hierarchies but are there realistic codebases where the class hierarchy would explode if tuples weren't used?

10 Answers

First of all, a tuple is quick and easy: instead of writing a class for every time you want to put 2 things together, there's a template that does it for you.

Second of all, they're generic. For example, in C++ the std::map uses an std::pair of key and value. Thus ANY pair can be used, instead of having to make some kind of wrapper class with accessor methods for every permutation of two types.

Finally, they're useful for returning multiple values. There's really no reason to make a class specifically for a function's multiple return values, and they shouldn't be treated as one object if they're unrelated.

To be fair, the code you pasted is a bad use of a pair.

Tuples are used all the time in Python where they are integrated into the language and very useful (they allow multiple return values for starters).

Sometimes, you really just need to pair things and creating a real, honest to god, class is overkill. One the other hand, using tuples when you should really be using a class is just as bad an idea as the reverse.

The code example has a few different smells:

  • Reinventing the wheel

There is already a tuple available in the framework; the KeyValuePair structure. This is used by the Dictionary class to store pairs, but you can use it anywhere it fits. (Not saying that it fits in this case...)

  • Making a square wheel

If you have a list of pairs it's better to use the KeyValuePair structure than a class with the same purpose, as it results in less memory allocations.

  • Hiding the intention

A class with properties clearly shows what the values mean, while a Pair<int,int> class doesn't tell you anything about what the values represent (only that they are probably related somehow). To make the code reasonably self explanatory with a list like that you would have to give the list a very desciptive name, like productIdAndQuantityPairs...

For what its worth, the code in the OP is a mess not because it uses tuples, but because the values in the tuple are too weakly typed. Compare the following:

List<Pair<Integer, Integer>> products_weak = blah1();
List<Pair<Product, Integer>> products_strong = blah2();

I'd get upset too if my dev team were passing around IDs rather than class instances around, because an integer can represent anything.


With that being said, tuples are extremely useful when you use them right:

  • Tuples exist to group ad hoc values together. They are certainly better than creating excessive numbers of wrapper classes.
  • Useful alternative to out/ref parameters when you need to return more than one value from a function.

However, tuples in C# make my eyes water. Many languages like OCaml, Python, Haskell, F#, and so on have a special, concise syntax for defining tuples. For example, in F#, the Map module defines a constructor as follows:

val of_list : ('key * 'a) list -> Map<'key,'a>

I can create an instance of a map using:

(* val values : (int * string) list *)
let values = 
    [1, "US";
     2, "Canada";
     3, "UK";
     4, "Australia";
     5, "Slovenia"]

(* val dict : Map<int, string> *)
let dict = Map.of_list values

The equilvalent code in C# is ridicuous:

var values = new Tuple<int, string>[] {
     new Tuple<int, string>(1, "US"),
     new Tuple<int, string>(2, "Canada"),
     new Tuple<int, string>(3, "UK"),
     new Tuple<int, string>(4, "Australia"),
     new Tuple<int, string>(5, "Slovenia")
     }

 var dict = new Dictionary<int, string>(values);

I don't believe there is anything wrong with tuples in principle, but C#'s syntax is too cumbersome to get the most use out of them.

It's code reuse. Rather than writing Yet Another Class With Exactly The Same Structure As The Last 5 Tuple-Like Classes We Made, you make... a Tuple class, and use that whenever you need a tuple.

If the only significance of the class is "to store a pair of values", then I'd say using tuples is an obvious idea. I'd say it was a code smell (as much as I hate the term) if you started implementing multiple identical classes just so that you could rename the two members.

This is just prototype-quality code that likely was smashed together and has never been refactored. Not fixing it is just laziness.

The real use for tuples is for generic functionality that really doesn't care what the component parts are, but just operates at the tuple level.

The obvious example is a coordinate pair (or triple). The labels are irrelevant; using X and Y (and Z) is just a convention. Making them uniform makes it clear that they can be treated in the same way.

Many things have already been mentioned, but I think one should also mention, that there are some programming styles, that differ from OOP and for those tuples are quite useful.

Functional programming languages like Haskell for example don't have classes at all.

If you're doing a Schwartzian transform to sort by a particular key (which is expensive to compute repeatedly), or something like that, a class seems to be a bit overkill:

val transformed = data map {x => (x.expensiveOperation, x)}
val sortedTransformed = transformed sort {(x, y) => x._1 < y._1}
val sorted = sortedTransformed map {case (_, x) => x}

Having a class DataAndKey or whatever seems a bit superfluous here.

Your example was not a good example of a tuple, though, I agree.

Related