This is the fifth in a series of 'use cases' for the closures proposal. This time, asynchronous callbacks.
Concurrent Executor
Executor ex = ... ex.execute() { // closure // invoked in another thread asynchronously }
This code takes a Java 5 Executor and executes it the behaviour asynchronously. This is clearly shorter than the current syntax:
Executor ex = ... ex.execute(new Runnable() { // inner class public void run() { // invoked in another thread asynchronously } });
It may be shorter, but is it clearer overall? Unfortunately, problems happen when you use break, continue or return. The following is a more complex asynchronous inner class:
public void process() { final AddressService service = ... final Person person = ... Executor ex = ... ex.execute(new Runnable() { // inner class public void run() { for (String addressId : person.getAddressIdList()) { Address address = service.getAddress(addressId); if (address == null) { return; } person.addAddress(addressId, address); } service.setRetrievedAddresses(true); } }); }
(OK, so this is probably stupid code - thats not the point here...). Now lets do a 'dumb' conversion to closures, following the rules we have learnt in previous blogs ("a closure works just like a synchronized block"):
public void process() { AddressService service = ... Person person = ... Executor ex = ... ex.execute() { // closure for (String addressId : person.getAddressIdList()) { Address address = service.getAddress(addressId); if (address == null) { return; } person.addAddress(addressId, address); } service.setRetrievedAddresses(true); } }
Unfortunately, one of two things will happen here. This will either fail to compile at the return statement or it will throw an UnmatchedNonLocalTransfer exception at runtime, also at the return statement. (The former happens if Runnable implements RestrictedClosure, the latter happens if it doesn't). In addition, the compiler or runtime will complain about the service and person variables being non-final.
Why is this an error? Because the 'return' keyword in a closure has been defined to mean 'return from the surrounding method' - the process() method in this case. But, this closure is being executed asynchronously, at a later point in time, on a different thread. Thus the process() method no longer exists and cannot be returned from. In other words, all the old inner class restrictions are still present. Its just that because the syntax changed, we were misled into thinking the restrictions had been removed.
How can this be solved? Well, one solution is refactor the closure method:
public void process() { final AddressService service = ... final Person person = ... Executor ex = ... ex.execute() { // closure boolean success = true; for (String addressId : person.getAddressIdList()) { Address address = service.getAddress(addressId); if (address == null) { success = false; break; } person.addAddress(addressId, address); } service.setRetrievedAddresses(success); } }
I'm sorry, but refactoring to use a success flag like that is just a bad code-smell. So I guess that we'd have to refactor to use another method. But that really defeats the purpose of inline, quick-and-simple closures.
The basic premise of closures from the previous 4 uses on this blog, was that you could just add code to the closure in the same way as you add code to a synchronized block. With asynchronous closures, that just isn't so. And I suspect that most developers would feel cheated as a result. Two different types of closure. Two different sets of syntax rules.
I could have written this blog without telling you that the execute closure was asynchronous. This would have produced real confusion, as there is no way of telling, just by reading the code that it is restricted (asynchronous) and therefore doesn't allow return, break or continue, or access to non-final variables.
Personally, I don't see how closures are viable in Java with this issue. Its just way too confusing. One solution would be to prevent asynchronous closures by controlling the reference to the closure object closely. A second possible solution is to use a different invocation syntax that clearly identifies the different rules - in fact, something not unlike CICE...:
public void process() { final AddressService service = ... final Person person = ... Executor ex = ... ex.execute(new Runnable() { for (String addressId : person.getAddressIdList()) { Address address = service.getAddress(addressId); if (address == null) { return; // means return from Runnable } person.addAddress(addressId, address); } service.setRetrievedAddresses(true); }); }
Summary
Asynchronous closures have different syntax rules to regular closures - no return, break, continue or non-final variable access. A developer with inner-class experience will be misled into believing that closures remove those restrictions, be severely disappointed when they discover the truth, and probably return to coding inner-classes for clarity. Developers without inner-class experience will likely be just plain confused as to why closures sometimes compile, sometimes don't and sometimes throw weird runtime exceptions.
No comments:
Post a Comment
Please be aware that by commenting you provide consent to associate your selected profile with your comment. Long comments or those with excessive links may be deleted by Blogger (not me!). All spam will be deleted.