Tuples and function types

Tuples

Ceylon is getting tuples in the next version. For those who don´t know what a tuple is, it´s a bit like a struct in C: a finite sequence of heterogeneous typed elements values, with no methods (unlike a class) and no names (unlike C structures). But let´s start with an example.

In Ceylon, [1, 2.0, "Hello"] is a tuple value of size 3 with type [Integer, Float, String]. Now let´s see a more useful example:

// convert a sequence of strings into a sequence of tuples with size, string
// and number of uppercase letters
Iterable<[Integer,String,Integer]> results = 
    {"Merry Christmas", "Happy Cheesy Holidays"}
        .map((String s) [s.size, s, s.count((Character c) c.uppercase)]);
for([Integer,String,Integer] result in results){
    Integer size = result[0];
    String s = result[1];
    Integer uppercaseLetters = result[2];
    print("Size: " size ", for: '" s "', uppercase letters: " uppercaseLetters "");
}

Which outputs:

Size: 15, for: 'Merry Christmas', uppercase letters: 2
Size: 21, for: 'Happy Cheesy Holidays', uppercase letters: 3

As you can see, you can access each tuple element using the Correspondence.item syntax sugar result[i], and that's because our Tuple type satisfies Sequence, underneath.

You may ask, but then what is the sequence type of a [Integer, String, Integer] tuple, then? Easy: it´s Sequencial<Integer|String>.

Then how do we possibly know that result[2] is an Integer and not an Integer|String? Well, that´s again syntactic sugar.

You see, our Tuple type is defined like this:

shared class Tuple<out Element, out First, out Rest>(first, rest)
        extends Object()
        satisfies Sequence<Element>
        given First satisfies Element
        given Rest satisfies Element[] {

    shared actual First first;
    shared actual Rest rest;

    // ...
}

So a tuple is just a sort of elaborate cons-list and result[2] is syntactic sugar for result.rest.rest.first, which has type Integer.

Similarly, the [Integer, String, Integer] type literal is syntactic sugar for:

Tuple<Integer|String, Integer, Tuple<Integer|String, String, Tuple<Integer, Integer, Empty>>>

And last, the [0, "foo", 2] value literal is syntactic sugar for:

Tuple(0, Tuple("foo", Tuple(2, empty)))

As you can see, our type inference rules are smart enough to infer the Integer|String sequential types by itself.

Function types

So tuples are nice, for example to return multiple values from a method, but that´s not all you can do with them.

Here´s how our Callable type (the type of Ceylon functions and methods) is defined:

shared interface Callable<out Return, in Arguments> 
    given Arguments satisfies Void[] {}

As you can see, its Arguments type parameter accepts a sequence of anything (Void is the top of the object hierarchy in Ceylon, above Object and the type of null), which means we can use tuple types to describe the parameter lists:

void threeParameters(Integer a, String b, Float c){
}

Callable<Void, [Integer, String, Float]> threeParametersReference = threeParameters;

So, Callable<Void, [Integer, String, Float]> describes the type of a function which returns Void and takes three parameters of types Integer, String and Float.

But you may ask what the type of a function with defaulted parameters is? Defaulted parameters are optional parameters, that get a default value if you don´t specify them:

void oneOrTwo(Integer a, Integer b = 2){
    print("a = " a ", b = " b ".");
}
oneOrTwo(1);
oneOrTwo(1, 1);

That would print:

a = 1, b = 2.
a = 1, b = 1.

So let´s see what the type of oneOrTwo is:

Callable<Void, [Integer, Integer=]> oneOrTwoReference = oneOrTwo;
// magic: we can still invoke it with one or two arguments!
oneOrTwoReference(1);
oneOrTwoReference(1, 1);

So its type is Callable<Void, [Integer, Integer=]>, which means that it takes one or two parameters. We´re not sure yet about the = sign in Integer= to denote that it is optional, so that may change. This is syntactic sugar for Callable<Void, [Integer] | [Integer, Integer]>, meaning: a function that takes one or two parameters.

Similarly, variadic functions also have a denotable type:

void zeroOrPlenty(Integer... args){
    for(i in args){
        print(i);
    }
}
Callable<Void, [Integer...]> zeroOrPlentyReference = zeroOrPlenty;
// magic: we can still invoke it with zero or more arguments!
zeroOrPlentyReference();
zeroOrPlentyReference(1);
zeroOrPlentyReference(5, 6);

Here, [Integer...] means that it accepts a sequence of zero or more Integer elements.

Now if you´re not impressed yet, here´s the thing that´s really cool: thanks to our subtyping rules (nothing special here, just our normal subtyping rules related to union types), we´re able to figure out that a function that takes one or two parameters is a supertype of both functions that take one parameter and functions that take two parameters:

Callable<Void, [Integer, Integer=]> oneOrTwoReference = oneOrTwo;

// we can all it with one parameter
Callable<Void, [Integer]> one = oneOrTwoReference;
// or two
Callable<Void, [Integer, Integer]> two = oneOrTwoReference;

And similarly for variadic functions, they are supertypes of functions that take any number of parameters:

Callable<Void, [Integer...]> zeroOrPlentyReference = zeroOrPlenty;

// we can call it with no parameters
Callable<Void, []> zero = zeroOrPlentyReference;
// or one
Callable<Void, [Integer]> oneAgain = zeroOrPlentyReference;
// or two
Callable<Void, [Integer, Integer]> twoAgain = zeroOrPlentyReference;
// and even one OR two parameters!
Callable<Void, [Integer, Integer=]> oneOrTwoAgain = zeroOrPlentyReference;

Although this is something that dynamic languages have been able to do for decades, this is pretty impressive in a statically typed language, and allows flexibility in how you use, declare and pass functions around that follows the rules of logic (and checked!) and don´t make you fight the type system.

By the way

I´ve shown here that we support method references in Ceylon, but the same is true with constructors, because after all a constructor is nothing more than a function that takes parameters and returns a new instance:

class Foo(Integer x){
    shared actual String string = "Foo " x "";
}
print(Foo(2));

Callable<Foo, [Integer]> makeFoo = Foo;
print(makeFoo(3));

Will print:

Foo 2
Foo 3