Bizarre Love TrianglePosted: September 30, 2011
Correct me if I’m wrong (and I’m sure I am), but I’m not sold on the Tuple and Function implementations in Scala. A Tuple is just a typed wrapper around multiple elements, allowing you to treat multiple objects as one object. Their difference with other wrapper/container constructs like List (or Collection in general) is that we know, at compile time, a) the number of elements and b) the type of each element. Functions follow a similar compile-time pattern, since you know the number and type of elements for the function at compile time. Tuples are very useful constructs – for example, you can imagine any n-arity Function as being a 1-arity Function taking a single TupleN argument; this elegance is expressed in Scala’s FunctionX.tupled, overloaded for functions of arity two through five. If you have some Traversable of Tuple2s, you can create a Tuple2 of Traversables holding the corresponding elements of each Tuple2 by simply calling Traversable.unzip. There’s a similar method for Tuple3’s called unzip3.
Unfortunately, design imperfections start to creep in when you try taking advantage of Tuples. None of the TupleNs have any class relation to each other except for all being subclasses of Product. I haven’t explicitly run into this problem yet, but it’s still troubling to think that Scala treats Tuple1._1, Tuple2._1, and Tuple3._1 as completely different properties. Product has a productElement(int) method that returns the nth element of the Product, but the guarantee is so weak that the method a) returns an Any and b) can only throw a runtime exception. productElement breaks both type safety and compile time guards, two of Scala’s stronger selling points. productIterator similarly returns an Iterator[Any]. I’m not sure why productIterator can’t return an iterator of the common supertype of all its elements. None of the FunctionN’s are even related to each other except for AnyRef. The Function object’s tupled, untupled, and uncurried methods have separate, overloaded implementations for Function2, Function3, Function4, and Function5. Function lacks a method returning the number of arguments it takes, which I feel should be easily done without copypasta. I guess the number of arguments is embedded within the Function’s type, but that’s programmatically annoying and unintuitive to access. unzip and unzip3 exist and satisfy most use cases, but it smells (as a consequence of the way Tuples are implemented). Code repetition is to be avoided – but the Scala core library itself contains lots of repetition on this front.
I feel like the problem lies in characterizing the relationship between Tuples of different sizes. We can think of any Tuple2 as a Tuple3 with an “empty” element (perhaps a Tuple3[A, B, Nothing] type or Tuple3[A, B, Unit]); a Tuple3 can also be seen as a Tuple4 with an “empty” element, etc. etc. One could put some upper limit on the Tuple arity (Scala’s already doing this) and have all other Tuples descend from that class, but there will still be a bunch of code boilerplate (create 21 classes, with each TupleN extending Tuple(N+1)), and the performance penalty of carrying a bunch of Units around would be hefty to say the least (perhaps specialization could help us here). Also, is Tuple3[A, Nothing, B] the same “thing” as Tuple3[A, B, Nothing]? I guess it depends on the application and programmer to assign meaning to the elements in the Tuple. If we instead build up, a Tuple3 can be seen as a Tuple2[A, Tuple2[B, C]]; a Tuple4 is a Tuple3[A, Tuple2[B, Tuple2[C, D]], or some similar Tuple nesting scheme, etc. etc. This looks to be a more reasonable approach to building Tuples, and is also the way Lisp and most of FP does it. Scala’s own List class has :: and Nil. The relation between Tuples and Lists is something to be explored. This connection is further complicated when Functions enter the scene, especially with regards to function currying. Naively, I think the problem lies in that there are multiple representations, in code, to achieve roughly the same meaning, and that there are no ways to easily convert from one representation to another. A Function3[A, B, C, R] and a Function1[(A, B, C), R] and a Function3[(A), (B), (C), (R)] and a Function2[(A, B), C, R] could all be the same thing. I don’t even want to get into the different ways you could group smaller Tuples to create a larger one.