Closures in Groovy
There is an interesting debate going on about closures in the Groovy
language. You can catch up on the discussion here:
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:
-
- […] And for Groovy at least, all closures look alike - two
closures which do radically different things and take different parameters
can be used interchangably from a static typing perspective. Clashes are
only detected at Runtime. By comparison an AIC is much more verbose in its
definition, but it is statically typed, so you can’t use two AICs
created against different interfaces interchangably. Dynamic typers will
scoff at this, but in larger programs being able to define a definite type,
parameters, and return type can be a huge boon. […]
Wow, a "huge boon". Let’s see how this boon works out in
practice.
Static Closure Types
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.
- Note:
- I’m using vaguely Groovy like syntax here, modified with a variation
of Mike’s suggested closure syntax. I am not a Groovy expert and may
get some details wrong. However, I don’t want to get hung up on the
syntax here, but want to concentrate on the ideas behind them. So bear with
me. Thanks.
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.
Relaxing the Exact Match Rule
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.
Summarizing the Closure Compatibility Rules
We can summarize the rules for assigning closures to closure variables as
follows:
A closure can be assigned to a closure variable when:
- The closure and the closure variable take the same number of arguments,
and
- the values passed in the argument list given to the closure variable
can be assigned to the formal parameters of the closure, and
- the return type of the closure can be assigned to a variable of the
return type of the closure variable.
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.
A Real Life Example
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.
The Example Was Rigged!
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?
Can We Fix This?
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.
Summary
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.
comments
|