{ |one, step, back| }

Statically Typed Closures In Groovy
21 Jan 05 - http://onestepback.org/index.cgi/Tech/Groovy/Closures.rdoc

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:

  1. The closure and the closure variable take the same number of arguments, and
  2. the values passed in the argument list given to the closure variable can be assigned to the formal parameters of the closure, and
  3. 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.