Friday, December 5, 2014

Polymer (Dart) Mixins Are Not a Real Thing


I don't know how it happened, but I realized today that I rather appreciate the expressiveness of Dart mixins. At first they seemed little more than a curiosity, but I find myself wanting to use them more and more. I am at the point that they are something of a Golden Hammer in my thinking. In other words, what I am trying tonight will almost certainly not work.

But it might.

I have my Polymer baseclass for converting other Polymer elements into native HTML form elements in decent shape. After last night, it even works as a package. So I ought to write some tests, publish it to Dart Pub and move onto other territory.

But what if I could use mixins to replace this syntax for using AFormInput:
import 'package:polymer/polymer.dart';
import 'package:a-form-input/a_form_input.dart';

@CustomTag('x-pizza')
class XPizza extends AFormInput {
  // Polymer code here...
}
A mixin version might look something like:
import 'package:polymer/polymer.dart';
import 'package:a-form-input/a_form_input.dart';

@CustomTag('x-pizza')
class XPizza extends PolymerElement with AFormInput {
  // Polymer code here...
}
I can appreciate that because it makes it more clear that my <x-pizza> element is a polymer element with form input traits mixed into the fun. In other words, <x-pizza> is-a PolymerElement with AFormInput mixed-in.

Maybe it is a small difference, but in my current form, there is no way of know what AFormInput is. Presumably, it is some kind of PolymerElement based on the @CustomTag annotation, but what kind of Polymer element it is remains a mystery (does it directly subclass PolymerElement? Extend another class that subclasses PolymerElement?). I also appreciate the abstract nature of mixins—they make it much harder to add cruft that belongs elsewhere.

This probably will not work because lifecycle methods like attached(), created(), and ready() all expect some kind of traditional inheritance. Still, it is worth a try.

So I define AFormInputMixin as:
library a_form_input;

import 'package:polymer/polymer.dart';
import 'dart:html';

abstract class AFormInputMixin {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
}
As I mentioned, mixin classes need to be abstract. I will need to add other functionality, but these properties are the absolute minimum needed to start.

In the concrete <x-pizza> element, I now mix this class in:
import 'package:polymer/polymer.dart';
// import 'a_form_input.dart';
// import 'package:a-form-input/a_form_input.dart';
import 'package:a-form-input/a_form_input_mixin.dart';

@CustomTag('x-pizza')
// class XPizza extends AFormInput {
class XPizza extends PolymerElement with AFormInputMixin {
  // ...
}
But, when I pub serve or build the code, I get:
[Warning from Observable on form_example|../../../../a-form-input-dart/lib/a_form_input_mixin.dart]:
line 7, column 3 of ../../../../a-form-input-dart/lib/a_form_input_mixin.dart: Observable fields must be in observable objects. Change this class to extend, mix in, or implement Observable. See http://goo.gl/5HPeuP#observe_5 for details.
@PublishedProperty(reflect: true)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Warning from Observable on form_example|../../../../a-form-input-dart/lib/a_form_input_mixin.dart]:
line 10, column 3 of ../../../../a-form-input-dart/lib/a_form_input_mixin.dart: Observable fields must be in observable objects. Change this class to extend, mix in, or implement Observable. See http://goo.gl/5HPeuP#observe_5 for details.
@PublishedProperty(reflect: true)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All right, that is fair enough. In order for the annotations to have an effect, I need for the class to be some kind of observable object. So I make the <a-form-input> a direct subclass of Observable:
abstract class AFormInputMixin extends Observable {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
}
I can now build the <x-pizza> element that mixes this in without warnings or errors, but when I try to access the element in Dartium , I get:
Internal error: 'package:form_example/elements/x_pizza.dart': error: line 13 pos 42: mixin class 'AFormInputMixin' must extend class 'Object'
class XPizza extends PolymerElement with AFormInputMixin, ChangeNotifier {
                                         ^
Bother.

Well, I know that Observable works as a mixin as well, so I should be able to extend Object (as the runtime wants) and mixin Observable (as compile time wants):
abstract class AFormInputMixin extends Object with Observable {
  @PublishedProperty(reflect: true)
  String name;

  @PublishedProperty(reflect: true)
  String value;
}
Extending Object like that is something of a cheat (OK it's entirely a cheat) because I am not mixing behavior into anything at this point—just the top-level object. I would definitely avoid that in concrete classes, but I can live with it inside another mixin.

Except it does not work. Even though I am explicitly extending Object, runtime still reports:
Internal error: 'package:form_example/elements/x_pizza.dart': error: line 13 pos 42: mixin class 'AFormInputMixin' must extend class 'Object'
class XPizza extends PolymerElement with AFormInputMixin, ChangeNotifier {
                                         ^
Gosh. Darn. It.

After some fiddling, I determine that this error has nothing to do with my explicit class declaration of the mixin. If I remove the @PublishedProperty annotations, then everything compiles and runs just fine. Well, just fine until my concrete Polymer element tries to use the no longer published value property:
Exception: Class 'XPizza' has no instance setter 'value='.

NoSuchMethodError: method not found: 'value='
Receiver: Instance of 'XPizza'
Arguments: ["First Half: []\nSecond Half: []\nWhole: []"]
Unfortunately, I cannot think of anything else to try at this point. At the risk of casting these grapes as sour, I still doubt that the lifecycle methods would have worked without some kind of mixin convention. All the same, this would have been fun. Maybe in a future version of Polymer.dart.


Day #15

5 comments:

  1. Try

    abstract class AFormInputMixin implements Observable

    That might work.

    ReplyDelete
    Replies
    1. Yeah, I tried that as well. Same result.

      The @PublishedProperty annotation adds ChangeNotifier as the baseclass if it's not already present, resulting in the must-extend-Object error no matter what.

      Delete
  2. I was going to comment the same thing. The type heirarchy for `MyType extends Object with MixinClass` is actually:

    MyType
    `- Object with MixinClass
    `- Object

    The `Object with MixinClass` is an artificial class inserted by the type engine to deal. What you actually want to do here is add the abstract properties to the interface, sans annotations

    ```
    class FormInput implements Observable {
    /// The name of the input. Conforming implementations should annotate the implementing field with `@PublishedProperty(reflect: true)`
    String get name;
    set name(String value);

    // etc.
    }
    ```

    Although it seems like you haven't gained anything of value (you still have to add the attributes as fields in the implementing class and annotate them correctly), you can add mixin methods which access the properties once they are implemented on the base class.

    Or you could just use an input element in the first place (`class MyInputElement extends InputElement with Polymer, Observable`)

    ReplyDelete
    Replies
    1. Thanks for the info -- I was not aware the artificial class added the hierarchy. I'll try to remember that in the future. I would guess that the implements approach would work, but agree that it doesn't really gain much if the concrete class has to declare the annotations. I may still mess around with it, but the lifecycle methods still won't work, so it does not seem like it'll be of much benefit.

      The MyInputElement suggest also seems interesting, but difficult to work in JavaScript (I'm really looking for solutions that work in both). Also, I am not just trying to get published properties -- I also perform work in Polymer lifecycle methods. Really, that's where the bulk of the baseclass / mixin code resides. I could mix that in with Polymer and Observable, but again, it seems like too much work will still occur in the concrete class.

      Still, it's fun messing around with this, so I may explore a bit more. Thanks for the suggestions!

      Delete
  3. Why not MyClass Extends HtmlElement with PolymerMixin, ObservableMixin?

    ReplyDelete