A method reference like Float.times is represented in curried
form in Ceylon. I can write:
Float twoTimes(Float x) = 2.times;
Here, the expression 2.times is a typical first-class function reference produced by the partial application of the method times() to the receiver expression 2.
But I can also write:
Float times(Float x)(Float y) = Float.times;
Actually, the expression Float.times is really a metamodel reference to a method declaration. The type Method<Float,Float,Float> is a subtype of Callable<Callable<Float,Float>,Float>, so we can treat it as a function reference.
Therefore, an alternative definition of twoTimes() is:
Float twoTimes(Float x) = Float.times(2);
(We're partially applying Float.times by supplying one of its two argument lists.)
Unfortunately, the following isn't correctly typed:
Float product(Float x, Float y) = Float.times; //error: Float.times not a Callable<Float,Float,Float>
The problem is that Float.times, when considered as a function reference, is a higher-order function that accepts a Float and returns a function that accepts a Float, not a first-order function that accepts two Floats.
So how can we transform the method reference Float.times into an uncurried
function with a single parameter list?
Well, one really simple way would be to fall back to writing:
Float product(Float x, Float y) { return x.times(y); //or even: x*y }
But, well, the purpose of this post is to demonstrate some fancy features of higher-order functions in Ceylon, so this isn't a very interesting solution. Instead, we're going to use a really cool higher-order function that will be part of the Ceylon language module. It's just two lines of code, so I'm sure you'll immediately understand it:
R uncurry<R,T,P...>(R curried(T t)(P... p))(T receiver, P... args) { return curried(receiver)(args); }
Whoah! Wtf?
Well, it's obviously time for you to re-read Part 8! Ok, done that? Cool, now let's try to unwind this:
- First, it's a function with two parameter lists, so uncurry()() is a function that returns a function.
- The first parameter list contains a single argument which also has two parameter lists, so the argument curried()() is also a function that returns a function.
- curried()() has an argument of form P..., a sequenced type parameter, so we know that curried()() is somehow abstracted over functions with arbitrary lists of parameters.
- The second parameter list contains two arguments, of the same types as the parameters of the individual arguments of the parameter lists of curried()(). These are the parameters of the function returned by uncurry()().
So what uncurry()() is doing is taking a function in curried form, where the second parameter list can have an arbitrary number of parameters, and producing a different function with just one parameter list, including all the original parameters of the argument function. It's flattening
the parameter lists of curried()() into a single list of parameters. So we can write the following:
Float product(Float x, Float y) = uncurry(Float.times);
Other kinds of operations on functions can be represented in a similar way. Consider:
R curry<R,T,P...>(R uncurried(T t, P... p))(T receiver)(P... args) { return uncurried(receiver,args); }
This function does precisely the opposite of uncurry()(), it takes the first parameter of an argument function, and separates it out into its own parameter list, allowing the argument function to be partially applied:
Float times(Float x)(Float y) = curry(product); Float double(Float y) = times(2.0);
Now consider:
R compose<R,S,P...>(R f (S s), S g(P... p))(P... args) { return f(g(args)); }
This function composes two functions:
Float incrementThenDouble(Float x) = compose(2.0.times,1.0.plus);
Fortunately, you won't need to be writing functions like curry()(), uncurry()() and compose()() yourself. They're general purpose tools that are packaged as part of the language module. Nevertheless, it's nice to know that machinery like this is expressible within the type system of Ceylon.