Sunday, December 27, 2015

On Second Thought, Extending Dart Generics

Extending Dart generics was new to me. And, having slept on it, I don't think I am a fan.

Following a suggestion last night, I briefly explored Dart generics as a way of limiting types in the command pattern. Do not mistake me here, I love the suggestion itself. As documentation, I think the generics suggestion works to a certain extent, though it was not obvious to me at first (perhaps it should have been so I'll let it slide for now).

So what is the particular implementation that has me bugged? It is extending a generic as in:
class ExtendedGenericB<T extends B> {
  T prop;
What this says is that my ExtendedGenericB class supports type annotation when instances are created. The prop instance variable must be of whatever type is supplied and it has to be a B class or one of B's subclasses.

That is, if I have three classes, A, B which is a subclass of A, and C which is a subclass of B:
class A {}
class B extends A {}
class C extends B {}
Then my extended generic declarations says that prop, which gets its type from the generic, must either a B or a C type. And at first blush that seems to be the case.

If I declare an instance of ExtendedGenericB with type parameter of A (a superclass of the declared B upper bound for my generic):
  var a = new A();
  new ExtendedGenericB<A>(a);
Then I get a static type warning as expected:
[warning] 'A' does not extend 'B' 
For what it's worth, this is the relevant section (14. Generics) from the Dart spec:
A type parameter T may be suffixed with an extends clause that specifies the upper bound for T. If no extends clause is present, the upper bound is Object. It is a static type warning if a type parameter is a supertype of its upper bound. The bounds of type variables are a form of type annotation and have no effect on execution in production mode.
So all seems well, right?

Things start to get a little murkier if I change the type parameter on the instance from A to B or even C:
  // Why does this work?
  new ExtendedGenericB<C>(a);
I get no warnings here even though I am specifying a type C for the instance's prop value while supplying an object of type A.

The explanation for this is that Dart is not really statically typed. Rather its types are unsound. That is, I could assign C c = new A() and would see no warnings. I understand why this is the case, though when I run into it in live code, I usually have to give it some thought.

Things get really strange when setting the upper bound as a typedef, as I tried last night. If I declare A with a void-return-no-argument call() method:
class A {
  void call() {}
Then I have created a function class. That is, I could create an instance of A, as in A a = new A();, then call it like a function: a(). Since it is a void-return-no-argument function, I can describe it with a typedef:
typedef void Command();
Where this gets really mind-bendy is when extending a generic with a typedef, which actually works:
class ExtendedGenericB<T extends Command> {
  T prop;
Now I can create an ExtendedGenericB with a class or function:
  var a = new A();
  // Works because Dart is unsound:
  new ExtendedGenericB<B>(a);

  // Works because class upper limits to a typedef
  new ExtendedGenericB<Command>((){ print('whoa!'); });
I have to admit that this is pretty cool. But I still find it hard to follow. I have a better grasp on it now that I have noodled it through. I remain skeptical that I would use it in real life and even more so that I would remember what it meant coming back to it six months later. That said, it seems interesting enough to allow to percolate another day or two before dismissing it to fringe coding practices.

Play with it yourself on DartPad:

Day #46

No comments:

Post a Comment