Polymorphic dispatch in Xtend
Polymorphic dispatch (or http://en.wikipedia.org/wiki/Multiple_dispatch or multimethods as its also called) is a programming language construct which chooses a code path based on runtime types instead of types that are inferred at compile. The poor man’s method of achieving such behavior would be to litter your code with prose like this:
if( x instanceof TypeA ) { (x as TypeA).exprA } else if( x instanceof TypeB ) { (x as TypeB).exprB } else ...
Xtend 2.x offers two much better constructs to do polymorphic dispatching:
- Through the use of the dispatch modifier for function defs – this construct is Xtend’s “official” polymorphic dispatch.
- Through the use of the switch statement and referring to types instead of cases.
These are better than the poor man’s method because they are declarative, i.e.: they express intent much more clearly and succinctly. Though both constructs have a lot in common, there are some marked differences and Best Practices for safe guarding type safety which I’ll discuss in the blog.
The example
Consider the following Xtend code – note that the syntax coloring is lacking a bit, but WordPress doesn’t fully understand Xtend – …yet…. Also note that since Xtend2.3 you can have more than one Xtend class in one file.
class CommonSuperType { ... } class TypeA extends CommonSuperType { ... } class TypeB extends CommonSuperType { ... } class TypeC extends CommonSuperType { ... } class UnrelatedType { ... } class Handler { def dispatch foo(TypeA it) { it.exprA } def dispatch foo(TypeB it) { it.exprB } def bar(CommonSuperType it) { switch it { TypeA: it.exprA TypeB: it.exprB } } }
For simplicity’s sake, let’s assume that exprA and exprB both return an int. The Xtend compiler generates two public methods in the Java class Handler – one for foo, one for bar. Both of these have the same signature: int f(CommonSuperType), where f = foo or bar. In addition, for each foo dispatch function, Xtend generates a public method with signature int _foo(t), where t=TypeA or TypeB – note the prefixed underscore. The actual polymorphic dispatch then happens in “combined” foo(CommonSuperType) method, actually through the previously demonstrated poor man’s method.
By the way: a user-friendly way to inspect the “combined” method is the Outline which will group the dispatch functions belonging together under the combined signature.
Note that the foo and bar method ends up with CommonSuperType as the type of its parameter. This is because CommonSuperType is the most specific common super type of TypeA and TypeB – deftly implied by the name – and Xtend infers that as the parameter’s type for the “combined” method. In general, Xtend will compute the most specific common super type across all dispatch functions, on a per-argument basis. In case of the bar method we declared ourselves what the parameter type is.
As demonstration, add the following code to the Handler class and see what happens:
def dispatch foo(TypeC it) { it.exprC }
(Assume that exprC again returns an int.)
The generated foo and bar methods are functionally nearly identical, the difference being that foo explicitly throws an IllegalArgumentException mentioning the unhandled parameter type(s) in its message, in case you called it with something that is a CommonSuperType but neither of TypeA nor of TypeB. The bar method does no such thing and simply falls through the switch, returning the appropriate default value: typically null but 0 in our int-case. To remedy that, you’ll have to add a default case which throws a similar exception, like so:
def bar(CommonSuperType it) { switch it { TypeA: it.exprA TypeB: it.exprB default: throw new IllegalArgumentException("don't how to handle sub type " + it.^class.simpleName) } }
In case you already have sensible default case, you’re basically out of luck.
Potential mistakes
Both approaches have their respective (dis-)advantages which I’ll list comprehensively below. In both cases, though, it’s relatively easy to make programmers’ mistakes. The most common and obvious ones are:
- The parameter type of the “combined” foo method is inferred, so if you add a dispatch function having a parameter type which does not extend CommonSuperType, then the foo method will wind up with a more general parameter type – potentially Object. This means that the foo method will accept a lot more types than usually intended and failing miserably (through a thrown IllegalArgumentException) on most of them. This is especially dangerous for public (which happens to be the default visibility!) function defs.
- Xtend will not warn you at editor/compile time about the “missing” case TypeC: it’s a sub type of CommonSuperType but not of TypeA nor of TypeB. At runtime, the bar method will simply fall through and return 0.
- The return type of the combined method is also inferred as the most specific common super type of the various return types – again, potentially Object. This is usually much less of a problem because that inferred type is checked against the parameter type of clients of the combined method.
This shows that these constructs require us to do a little extra to safe guard the type safety we so appreciate in Xtend.
Advantages and disadvantages of both constructs
We list some advantages and disadvantages of both constructs. Advantages of the dispatch construct:
- Provides more visual code space. This is useful if the handling of the separate types typically needs more than 1 line of code.
- Explicit handling of unhandled cases at runtime.
Disadvantages of the dispatch construct:
- Automagically infers parameter types of the “combined” method as the most common super types. In case of a programmer error, this may be (much) too wide.
- Takes up more visual code space/more syntactic noise.
Advantages of the switch construct:
- Takes up less visual code space. This is useful if the handling of the separate types doesn’t need more than one line of code.
- It’s a single expression, so you can use it as such inside the function it’s living in. Also, you can precompute “stuff” that’s useful for more than one case.
Disadvantages of the switch construct:
- Fall-through of unhandled cases at runtime, resulting in an (often) non-sensical return value. You have to add an explicit default case to detect fall-through.
Mixing polymorphic and ordinary dispatch
Since Xtend version 2.3, you are warned about dispatch functions having a compatible signature as a non-dispatch function and vice versa. As an example, consider the following addition to the Handler class:
def foo(TypeC it) { it.exprC } def foo(UnrelatedType it) { it.someExpr }
Here, exprC again returns an int, but someExpr may return anything. Note that both functions are not of the dispatch persuasion.
The first line is flagged with the warning “Dispatch method has same name and number of parameters as non-dispatch method”, which is a just warning in my book. However, this warning is also given for the second line, as well as for the first two foo functions. (Note that the warnings are also given with only one of these extra functions present.) In that case, it’s not always a helpful warning but it does riddle your code file with warnings.
To get rid of the warnings, I frequently make use of the following technique:
- “Hide” all dispatch functions by giving them (and only these) an alternate name. My personal preference is to postfix the name with an underscore, since the extra _ it’s visually inconspicuous enough to not dilute the intended meaning. Also, give them private visibility to prevent prying eyes.
- Create an additional function with the same signature as the “combined” method for the dispatch functions, calling those.
The net result is that you get rid of the warnings, because there’s no more mixture of dispatch and non-dispatch functions with compatible signatures. Another upshot is that the signature of the “combined” method is now explicitly checked by the additional function calling it – more type safety, yeah! Of course, a disadvantage is that you need an extra function but that typically only is one line of code.
In the context of our example, the original two foo functions are replaced by the following code:
def foo(CommonSuperType it) { foo_ } def private dispatch foo_(TypeA it) { it.exprA } def private dispatch foo_(TypeB it) { it.exprB }
-
December 6, 2012 at 1:35 pmUsing Xtend with Google App Engine « Meinte's DSL Blog