For those who don’t want to read the whole conversation, I’ll summarize the bit I wish to address. Mike is advocating simplification of the Groovy language syntax to remove ambiguities and make have special cases. An admirable goal, no doubt.
In addition revoking the optional parenthesis and semi-colons currently sported by Groovy, Mike wants to make the closure syntax more explicit. Currently closures in Groovy look very similar to blocks in Ruby.
list.each { item | println(item) }
Mike would like to see it changed to …
list.each (clos(item) { println(item) });
Where clos is now a keyword that introduces the closure and the closure argument list is given explicitly in a way that is more Java-like. One of the reasons he likes this is that it opens the possibility of declaring the types of the arguments to the closure.
Quoting from the third link above:
Wow, a "huge boon". Let’s see how this boon works out in practice.
We will start this Mike’s suggested syntax add a return type declaration. This gives us the ability to declare closures like this:
# A closure that takes an integer and returns an integer value.
clos(int i):int { i + 1 }
# A closure that takes a string and returns a integer.
clos(String s):int { s.length() }
# A closure that takes a string and returns no value
clos(String s):void { println(s) }
I think you get the idea.
For static typing to work, we need the ability to declare variables of a given closure type. Here we declare three variables that match the types of the three closures above.
clos(int):int integer_func; clos(String):int string_func; clos(String):void handler;
Given the above declarations, we should be able to do the following:
integer_func = clos(int i):int { i + 1 }
string_func = clos(String s):int { s.length() }
handler = clos(String s):void { println(s) }
In all cases, the type of the closure exactly matches the declared type of the variable. We will be able to invoke the closures and be certain that there will be no runtime type errors, which is certainly the goal of static typing.
Consider the following …
interface Animal { void talk(); }
class Cat implements Animal { void talk() { println("Meow") }
Should the following assignment be allowed?
clos(Cat):void cat_handler
cat_handler = clos(Animal a):void { a.talk(); }
Let’s think about this. The closure can handle any type of Animal. The closure variable cat_handler is declared in such a way that only Cat objects can be passed to the closure referenced by cat_handler. Since Cats are Animals, this should cause no type errors at run time.
One more example, then we well summarize the rules.
clos():Animal factory;
factory = clos():Cat { new Cat() }
Again, this looks to be type safe. Closures called through the factory variable are guaranteed to return Animals. The closure in question returns a Cat object, which is certainly an Animal. This will cause no type errors at run time.
We can summarize the rules for assigning closures to closure variables as follows:
A closure can be assigned to a closure variable when:
The above language is way too informal for a real language definition, but I’m trying to get at understanding rather than exact semantics. The rules should do for our purposes.
We now have enough to try some real life examples. Consider the following transform function that takes a list and returns a new list built from an arbitrary transformation on each element. The transform is specified by a statically typed closure.
List transform(List list, clos(Object):Object transformation) {
List result = [];
for (Object current_obj in list) {
Object new_obj = transformation(current_obj);
result.append(new_obj);
}
return result;
}
Our closure is specified in terms of Object because we want it to work on any kind of list.
How would we use it? We might like to do the following:
clos(int):int t = clos(int i):int { i + 1 };
transform([1,2,3], t);
Oops! This doesn’t work. The closure t is not compatible with the closure needed by the transform function (it breaks rule 2 above). The closure t only takes arguments of type int, but transform could possibly pass any type to t.
Ok, it is pretty disappointing that the direct approach does not work. So let’s try something else. What if we tried this.
clos(Object):int u = clos(Object obj):int { ((int)obj) + 1 };
transform([1,2,3], u);
This works! But at what cost? Are you bothered that we needed a cast to get our closure to work properly? You probably should be, for what we have done is lie to the compiler, telling it our closure takes an arbitrary object when in fact it must have an integer (or something that can be converted to an integer).
If fact, we can now write code that is statically type valid, but fails at run time (with a class cast exception):
u("hello")
If our statically typed closures can’t lead to code that is runtime type-safe, then what is the advantage of bothering with static declarations. We might as well stay with the dynamically typed closures that offer the same amount of type safety and are much less complex.
Well, maybe. The problem lies in trying to use statically typed closures with functions that take generic arguments. But isn’t that exactly the situation where static type safety is most needed? If statically declared closures can’t handle the hard problems, why bother with them on the easy ones?
Perhaps. I believe it is possible to define statically typed closures. In fact, this article lays out how to do it in Eiffel (the agents described in the article are essentially closures). But the Eiffel language supports generics (and in particular generic Tuples) to work around the problems outlined here. Adding that to the language makes it even more complicated.
At first glance, statically typed generics look really attractive, but add a great deal of complexity without achieving static type safety for a very common class of problems where closures are commonly used. I just don’t see the advantage.
Mike Spille says "On typing, it’s a double-edged sword if you’re coming from a Java perspective.", and he is right. And you really have to be careful not to cut yourself on that sword.