Tuples, or not?

Posted by    |       Ceylon

A number of people have asked if Ceylon will have tuples. Well, I suppose, why not? It's easy enough to write the following generalized algebraic datatype:

shared interface TupleOrUnit<P...> 
        of Tuple<P...> | unit {}  //note: depends upon support for GADTs
shared class Tuple<T,P...>(T head, TupleOrUnit<P...> tail) 
        satisfies TupleOrUnit<T,P...> {
    shared T head = head;
    shared TupleOrUnit<P...> tail = tail;
}
shared object unit 
        extends Case("unit") 
        satisfies TupleOrUnit<> {}

Note that the definition of TupleOrUnit here depends upon compiler support for generalized algebraic datatypes, since the types that appear in the of clause don't cover every possible list of arguments to P.... We're trying to get the compiler to reason that the type of Tuple.tail is Tuple when there is more than one type argument. But even if we don't have support for GADTs, we can still get tuple support by removing the of clause of TupleOrUnit, and adding an introduction. It's a slightly ugly workaround, but doesn't affect the client. Anyway, the code winds up looking like this:

shared interface TupleOrUnit<P...> {}
shared class Tuple<T,P...>(T head, TupleOrUnit<P...> tail) 
        satisfies TupleOrUnit<T,P...> {
    shared T head = head;
    shared TupleOrUnit<P...> rest = tail;
}
shared object unit satisfies TupleOrUnit<> {}
shared interface TupleTail 
        adapts Tuple<T,P...> {
    shared Tuple<P...> tail { 
        if (is Tuple<P...> rest) {
            return rest;
        }
        else {
            //someone else extended TupleOrUnit directly
            throw Exception("unexpected subtype of TupleOrUnit");
        }
    }
}

Anyway, whichever path we take, you can now create a tuple like this:

local labeledPosition = Tuple(x,Tuple(y,Tuple(label,unit)));  //inferred type Tuple<Float,Float,String>

And access its elements like this:

Float x = labeledPosition.head;
Float y = labeledPosition.tail.head;
String name = labeledPosition.tail.tail.head;

Well, that's all a bit verbose, so we probably want to add some convenience functions for working with pairs and triples:

shared Tuple<X,Y> pair<X,Y>(X x, Y y) { return Tuple(x,Tuple(y,unit)); }
shared Tuple<X,Y,Z> triple<X,Y,Z>(X x, Y y, Z z) { return Tuple(x, pair(y,z)); }

shared T first<T,P...>(Tuple<T,P...> tuple) { return tuple.head; }
shared T second<S,T,P...>(Tuple<S,T,P...> tuple) { return tuple.tail.head; }
shared T third<R,S,T,P...>(Tuple<R,S,T,P...> tuple) { return tuple.tail.tail.head; }

Which let's us simplify the examples above to:

local labeledPosition = triple(x,y,label);
Float x = first(labeledPosition);
Float y = second(labeledPosition);
String name = third(labeledPosition);

Now, all that's pretty reasonable, and perhaps we could provide something like this code as part of the language module, but the truth is that isn't quite what people are asking for. What they want is sugar. For tuples to be convenient enough to be worthwhile, I think you really need some additional syntactic support for them in the language definition:

  • A nicer way to instantiate them, using a simplified syntax like this:
    local labeledPosition = (x,y,label);
  • Support for destructuring tuples in method parameter lists, for loops, and probably even in the LHS of specifier statements:
    for ((Float x, Float y, String label) in labeledPositions) { ... }
    (Float x, Float y, String label) = labeledPosition;
  • An abbreviated syntax for writing tuple types:
    (Float,Float,String) labeledPosition;

Well, that's a whole lot of extra syntax for a language feature that I find to be of pretty marginal value in an object-oriented language like Ceylon. I mean, I can see a couple of reasonable usecases for generic pairs and triples, but I really don't think we should be encouraging folks to write code with methods that return 4-tuples.

And for the particular case of pairs that represent entries in some kind of associative array, we do already have special sugar for Entrys, with:

  • The convenient -> operator for instantiating them, and
  • destructuring support in method parameter lists and for loops.

So I'm inclined to think that this feature isn't worth the extra complexity. It's just something extra to learn, and something that can be easily misused at that. But I'm reserving the right to change my mind somewhere further down the line!


Back to top