In a previous post I described a type of impedance mismatch between Javascript and Java idioms that makes the GWT Exporter still less than ideal for supporting Javascript programmers. A short illustrative (but contrived) example:
public class Customer implements Exportable {
private String firstName, lastName;
@Export String getFirstName() {
return firstName;
}
@Export void setFirstName(String fn) {
firstName = fn;
}
@Export String getLastName() {
return lastName;
}
@Export void setLastName(String ln) {
lastName = fn;
}
}
Today, you may use this export in Javascript like this:
var cust = new Customer();
cust.setFirstName("Ray");
cust.setLastName("Cromwell");
Processor.doSomething(cust);
The example is contrived because you could use a constructor, but with more complex objects with nested types, you wouldn't use a constructor, but either a builder pattern, or inject the types after construction.
Javascript developers however don't work in the world of Javabean interfaces, they prefer easy construction of configuration/builder information via object literals:
Processor.doSomething({firstName: "Ray", lastName: "Cromwell"})
Moreover, when passing in say, a bind of event callbacks, they'd prefer to write:
foo.addEvents({
click: function(e) { ... }
move: function(e) { ... }
drop: function(e) { ... }
});
The challenge is to seemlessly bridge this idiomatic mismatch between JS and Java GWT code without actually having to write much bridging code or adapters.
Structural Typing
Java is a manifestly typed language. All types have to be declared, and type checking is done by explicit hierarchy. Javascript is a dynamically typed language, with essentially no type checking at all. Orthogonal to this is the concept of Structural Typing. Haskell, ML, and Scala are all examples of languages which support structural typing. Structural typing was also planned for Javascript 2 before it got killed.
So what's a structural type? Recall the
Customer
example from the previous section. It was a class with two fields, firstName, and lastName, both Strings. If Java supported a structural type system, I could declare a method in two ways:
public void process(Customer cust) { ... }
public void process({firstName: String, lastName: String} cust) { ... }
In this invented syntax, the
cust
parameter to the second process()
function is an anonymous type, we don't know its real name. However, we are stating that as long as it consists of two fields named "firstName"
, and "lastName"
, and the types are both String
s, then we can access these fields and treat it like a Customer (although it may not be one)Hmm....I smell an idea...
Structural Type Exports
What if I rewrite the Customer POJO class with a
@StructuralType
annotation:
@Export
@StructuralType
public class Customer implements Exportable { ... }
The dispatch code for an exported
process()
function could then look like this:
$wnd.Processor.prototype.process = function(cust) {
if(cust instanceof $wnd.Customer && isExportedInstance(cust)) {
// JSNI dispatch to @Processor::process(LCustomer;)(unwrap(cust));
}
else if(typeof(cust) == 'object') {
// cust is not an instance of the Customer POJO, but an object
if(cust.firstName != undefined && cust.lastName != undefined) {
var scust = new Customer();
scust.setFirstName(cust.firstName);
scust.setLastName(cust.lastName);
@Processor::process(unwrap(scust));
}
}
}
The GWT compiler would auto-inject this structural type check and initialization code simply by annotating a parameter or return type involved with
@StructuralType
Refinements to the idea
Taking this a step further, one could override the expected type literal field names to be checked
@SType("fn")
public void setFirstName(String firstName) { ... }
@SType("ln")
public void setLastName(String lastName) { ... }
which would allow the object literal to be specified as
{fn: "Ray",ln: "Cromwell"}
. Another extension would allow partial matches to succeed with default values supplied:
@Optional("Ray")
public void setFirstName(String firstName) { ... }
which would reduce the structural type to just a
lastName
field for matching purposes, but would allow the specification of firstName
to be supplied and injected into the setter as "Ray" if it wasn't present.Structurally Typed Interfaces
GWT Exporter already supports closure conversion for single-method Java interfaces. That is, if the following interface:
public interface MyCallback {
public void go(Object arg);
}
occurs as a type parameter in an exported method, the Javascript developer may supply
function(arg) { ... }
and the GWT generated bridge code will automatically convert this into a GWT object instance that implements the MyCallback
interface type. What it cannot support is the example given earlier:
foo.addEvents({
click: function(e) { ... }
move: function(e) { ... }
drop: function(e) { ... }
});
which would represent something like:
public interface MyEventHandler {
public void onClick(Event e);
public void onMove(Event e);
public void onDrop(Event e);
}
However, using structural typing conventions, and an
@StructuralType
annotation on an interface, we can auto-convert object literals containing multiple function closures into a Java interface for GWT.I am still working out the details of the full overload resolution algorithm for overloaded function types, but some version of this proposal will make it into GWT Exporter 3.0.
-Ray
No comments:
Post a Comment