Friday, September 08, 2006

Anonymous interfaces and methods

After reading Neal Gafter's Closures for Java proposal I started thinking about a way to make it possible to change existing APIs to use the new syntax without breaking backward-compatiblity.


Let's introduce the following notions: named class, named interface, anonymous class, anonymous interface, named method, anonymous method. Java currently has named classes and interfaces, anonymous classes, and named methods. I suggest to add anonymous interfaces and anonymous methods. A named class or interface can have only named methods, but an anonymous class or interface can have both named and anonymous methods. In this terminology a function type is simply a one-method anonymous interface: the one method can be either named or anonymous.


For example, {int (String s)} denotes an anonymous interface with an anonymous method and {int foo(String s)} denotes an anonymous interface with a named method. Although one-method anonymous interfaces are likely to be the most useful, we can have any number of methods, named or anonymous in an anonymous interface. For example, { int (String s); int foo(String s);} is an anonymous interface with two methods. We can omit the curly braces and the semicolon if there is a single method. So for example int (String s) is a shorthand for {int (String s);}.


The construct int (String s) is different from the original proposal in two respects: there is variable name for the paramter (because that's how methods are defined in Java) , and foo is the name of a method. In the original proposal the expression for the function type is int foo(String), and foo denotes a variable.


Overloading rules are as usual if we define the "name" of all anonymous methods to be some fixed marker name which is different from any legal Java method name. So { int (String s); int foo(String s);} is OK but { int (String s); int (String s);} is not.


I want now to define when a type T1 is a subtype of T2 where T1 and T2 are classes or interfaces. But before that, I need to define when a method m1 is a subtype of a method m2. I take the same rules as specified in the original proposal for function types, but with the following addition: If m2 is a named method then m1 must be also named and have the same name as m2. So for example, int foo(String s) is a subtype of int (String s) but int (String s) is not a subtype of int foo(String s). The reason for that will become clear below.


Given a set of overloaded methods M = { m1(A1 a1,...,An an), m1(B1 b1,...,Bn bn) ...} such that all methods are subtypes of a method m2(P1 p1,...,Pn pn), we can define the notion of a "best match" for m2 in M by "inversing" the usual Java rules of resolving calls to overloaded methods. We say that the method m1(T1 t1, ...,Tn tn) in M is the best match for m2(P1 p1, ...,Pn pn) if the expression m1(p1,...,pn) resolves in a call to m1(T1 t1,...,Tn tn) by the usual Java rules.


We can now define when T1 is a subtype of T2 where T1 and T2 are classes or interfaces:


  • If T1 and T2 are both named then the usual Java rules apply (T1 is a subtype of T2 if T1 extends or implements T2 or a subtype of T2).

  • If T1 or T2 or both are anonymous then T1 is a subtype of T2 if for each method m2 in T2 there exists a method m1 in T1 such that m1 is a subtype of m2 (as defined above), and if there is more than one such method in T1 then there exists a unique best match (as defined above) for m2 in T1.



For example, { void (String s, Object o); void (Object o1, Object o2); } is a subtype of {void (String s1, String s2)} but { void (String s,Object o); void (Object o,String s); } is not because in the second case the method matching void (String s1,String s2) cannot be resolved unambiguously.


With these definitions we can now see how we can change an existing API to use "function types" (which are now anonymous interfaces) without breaking backward-compatiblity.


We can change void api(Runnable obj) to void api(void run() obj) and this change is backward-compatible. The paramter type is now the anonymous interface {void run();}. A client can call the new api method in the following ways:


  • Old way - pass a Runnable object. For example: api(new Runnable(){...}). This is still OK because Runnable is a subtype of the anonymous interface {void run();}
  • .
  • New ways - pass anything that is a subtype of {void run();}. For example: api(System.out.println("hello world");). Here the paramter passed is an anonymous class which is also a subtype of {void run();}.



Now this is maybe not exactly what you want - you want to change the API to void api(void () obj). api also doesn't care what the method is called. However, the latter change will not be backward-compatible. By the above definitions, Runnable is not a subtype of {void ();} because the method void run() is not a subtype of the anonymous method void () .


Why not allow void run() to be a subtype of void () ?
The reason is that if we allowed that then adding new methods to any class will potentially break backward-compatibility. Suppose for example you had a class with a single method void doThis() and you could pass it as a paramter to void api(void () obj). If you now add a new method void doThat() then the class is no longer a subtype of {void ();} and the call to void api(void () obj) is ambiguous . This is the reason why a named method is not allowed to be a subtype of an anonymous method.


Still it looks a pity that you cannot pass a Runnable to void api(void () obj). Perhaps this is not so bad: if you have a Runnable obj you want to pass, you can wrap it with an anonymous interface like this: api(obj.run()). Indeed, you can now replace void api(Runnable obj) with two methods: void api(void () obj) (the method you really want to define) and void api(void run() obj) (the method you must have for backward-compatibility). You can put the real implementation in void api(void run() obj) and have void api(void () obj) simply call api(obj.run()). This has the effect of "selecting" the void () method of obj to use: you select the run method and not any other method with the same signature.
(Note: An expression such api(System.out.println("hello world");) will then resolve in a call directly to void api(void () obj)).


Probably the two-methods suggestions above is not acceptable for an API in the SDK because of the overhead of creating an additional object implied by the expression api(obj.run()). Maybe there is a way to make such an expression really mean "select the run method" (or maybe there should be some new syntax for that, for example: obj#run()). I don't know if it's feasible for the compiler to implement such a "selection" syntax so that at runtime an additional object is not created. But even if it's not feasible, you can still compromise on the void api(void run() obj) method because of the backward-compatiblity constraints and use really anonymous methods only for new APIs. Still api(void run() obj) allows you to pass in an unnamed expression or block of code to run (which was the whole point) or anything that is "run"-able, that is - any class or interface with a void run() method.

0 Comments:

Post a Comment

<< Home