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.
very nice.
How exactly does that work with methods taking parameters of different types...hold the phone. How did I miss reading about sequenced type parameters? (P... above). Very nice.
ok, so let's try this again with a java-esque map Map<String, Integer> map:
or in other words
//remember: R uncurry<R,T,P...>(R curried(T t)(P... p))(T receiver, P... args); Integer putter(Map<String, Integer> map)(String key, Integer value) = Map.put; // P... gets both String and Integer, or in other words Integer store(Map<String, Integer> m, String key, Integer value = uncurry<Integer, Map<String, Key>, String, Integer>(Map.put);These sequenced type parameters are very useful. But how do I represent this method?
Right, your code for putter and store looks correct to me.
Currently no way to do it. A TODO in the spec speculates about the possibility of something like this:
Tuple<P...>[] zip<P...>(P[]... sequences) { ... }or even better, this:
T[] zip<T,P...>(T element(P... args), P[]... sequences) { ... }Which you would call like this:
But I think that would be a long way in the future.
Is this correct? Shouldn't it be
instructions. Then edit this text and check the preview.
I'm using times() as the name of the curried function, product() as the name of the uncurried version.
Aaah, times() is just a global function of course and does not have anything to do with the times() from Float (except for being defined in terms of it). I just got confused because of the similarity.