Sunday, May 12, 2013

Reorganizing Dart Code with Parts

‹prev | My Chain | next›

I think I probably ought to use Dart “parts” in the ICE Code Editor. Parts are used to organize a library like ICE. When I first started writing the Dart version of the editor, I mostly followed along with the old JavaScript version. JavaScript, of course, has no formal mechanism for code organization so code written in it is hardly a worthwhile guide. Now that I have two classes in ICE and am ready to add another, it seems time to give this some consideration.

Without parts, developers would have to import classes individually:
import 'package:ice_code_editor/editor.dart';
import 'package:ice_code_editor/embedded.dart';
import 'package:ice_code_editor/full.dart';
import 'package:ice_code_editor/store.dart';
// ...
new Editor();
new Embedded();
new FullScreen();
new Store();
If the individual classes were all in a single file, then only one import would be necessary:
import 'package:ice_code_editor/ice.dart';
// ...
new Editor();
new Embedded();
new FullScreen();
new Store();
Now, I do not think that most use-cases of ICE warrant using more than a single class at a time. Still, reducing the import to a single line would make documentation easier and would make it easier for developers to remember the package path.

Of course, it would be horrendous from a code maintenance standpoint to put everything in a single file—no one in their right mind thinks a thousand line long file of code is anything even approaching maintainable. This is the very point of Dart parts. They let me write the different classes in their own files, but mark them as being a part of a single library file.

In this case, I want to make the editor.dart and stort.dart file part of the main ice.dart file that will be imported into other code. So, in ice.dart, I indicate that these two files are parts:
library ice;

part 'editor.dart';
part 'store.dart';
The editor.dart file needs to change to support this. It is no longer a standalone library. It is now part of the ice library. So I need to remove the library declaration at the top of editor.dart:
library ice_editor;

import 'dart:html';
import 'dart:async';
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;

class Editor {
  // code here...
}
And mark it as a “part of” ice:
part of ice;

import 'dart:html';
import 'dart:async';
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;

class Editor {
  // code here...
}
That's not the end of it. Parts cannot have import statements, so I remove them from editor.dart:
part of ice;

class Editor {
  // code here...
}
After making similar changes to store.dart, I move all of import statements into ice.dart:
library ice;

import 'dart:async';
import 'dart:collection';
import 'dart:crypto';
import 'dart:html';
import 'dart:json' as JSON;
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;

part 'editor.dart';
part 'store.dart';
Lastly, I update my tests to import the new ice.dart:
import 'package:unittest/unittest.dart';
import 'dart:html';
import 'package:ice_code_editor/ice.dart';
// ...
With that, I have my code nicely organized and all my tests passing. Unfortunately, this makes dart_analyzer profoundly unhappy. Since I use Emacs for all of my editing, I rely on the dart_analyzer to ensure that I have not made any silly type errors. For instance, if I called a non-existent method on a List:
part of ice;
class Store implements HashMap<String, HashMap> {
  // ...
  // There is no "iEmpty" method on the projects List:
  bool get isEmpty => projects.iEmpty;
  // And projects is a List:
  List get projects { /* ... */ }
}
Then, I expect that dart_analyzer will complain:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart
file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of List<dynamic> (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
    57: 
    58:   bool get isEmpty => projects.iEmpty;
                                       ~~~~~~
Instead, I get a long list complaining about all of the JavaScript methods that were not declared to be a part of the js-interop Proxy class:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart                                                                                          file:/home/chris/.pub-cache/hosted/pub.dartlang.org/js-0.0.22/lib/js.dart:78:1: dart:mirrors is not fully implemented yet
    77: import 'dart:isolate';
    78: import 'dart:mirrors';
        ~~~~~~~~~~~~~~~~~~~~~~
file:/home/chris/repos/ice-code-editor/lib/editor.dart:172:18: "ace" is not a member of Proxy (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
   171:     scripts.first.onLoad.listen((event) {
   172:       js.context.ace.config.set("workerPath", "packages/ice_code_editor/js/ace");
                         ~~~
file:/home/chris/repos/ice-code-editor/lib/editor.dart:221:40: "Proxy" has no method named "setFontSize" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
   220:
   221:   set fontSize(String size) => $unsafe.setFontSize(size);
                                               ~~~~~~~~~~~
file:/home/chris/repos/ice-code-editor/lib/editor.dart:222:38: "Proxy" has no method named "setTheme" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
   221:   set fontSize(String size) => $unsafe.setFontSize(size);
   222:   set theme(String theme) => $unsafe.setTheme(theme);
                                             ~~~~~~~~
file:/home/chris/repos/ice-code-editor/lib/editor.dart:223:44: "Proxy" has no method named "setPrintMarginColumn" (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
   222:   set theme(String theme) => $unsafe.setTheme(theme);
   223:   set printMarginColumn(bool b) => $unsafe.setPrintMarginColumn(b);
                                                   ~~~~~~~~~~~~~~~~~~~~
...
(this goes on for quite some time)

So I break out sed to not print (!p) lines that fall between two matches. In all, there are four different type of errors that I find that I care nothing for, so the dart_analyzer + sed command becomes:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/ice.dart 2>&1 | \
  sed -n '/js-0.0.22\/lib\/src\/wrapping/,/~~/!p' | \
  sed -n '/is not a member of Proxy/,/~~/!p' | \
  sed -n '/"Proxy" has no method named/,/~~/!p' | \
  sed -n '/dart:mirrors is not fully implemented yet/,/~~/!p'

file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of List (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
    57: 
    58:   bool get isEmpty => projects.iEmpty;
                                       ~~~~~~
That's a lot to be typing each time, so I bundle that into an internal tool for the package, which pub package convention says goes in the tools sub-directory. So tools/js_dart_analyzer becomes:
#!/usr/bin/env sh

dart_analyzer $* 2>&1 | \
  sed -n '/js-0.0.22\/lib\/src\/wrapping/,/~~/!p' | \
  sed -n '/is not a member of Proxy/,/~~/!p' | \
  sed -n '/"Proxy" has no method named/,/~~/!p' | \
  sed -n '/dart:mirrors is not fully implemented yet/,/~~/!p'
With that, I have a much saner time running a js-interop safe version of the dart_analyzer tool:
➜  ice-code-editor git:(master) ✗ ./tool/js_dart_analyzer lib/ice.dart 
file:/home/chris/repos/ice-code-editor/lib/store.dart:58:32: "iEmpty" is not a member of List (sourced from file:/home/chris/repos/ice-code-editor/lib/ice.dart)
    57: 
    58:   bool get isEmpty => projects.iEmpty;
                                       ~~~~~~
The last thing that I do during this code re-organization is to put these parts to good use. While working in the Store class, it has become apparent that I have misplaced my Gzip'ing utilities. I had thought them intimately part of Store, which is why I had included them in the class:
part of ice;
class Store implements HashMap<String, HashMap> {
  // ...
  static String encode(String string) {
    var gzip = js.context.RawDeflate.deflate(string);
    return CryptoUtils.bytesToBase64(gzip.codeUnits);
  }
  static String decode(String string) {
    var bytes = CryptoUtils.base64StringToBytes(string);
    var gzip = new String.fromCharCodes(bytes);
    return js.context.RawDeflate.inflate(gzip);
  }
  // ...
}
Now it is apparent that the Store might make use of these js-interop methods, but they really belong in a separate, Gzip class. And, since parts make it so darn easy to organize code, I create gzip.dart with:
part of ice;

class Gzip {
  static String encode(String string) {
    var gzip = js.context.RawDeflate.deflate(string);
    return CryptoUtils.bytesToBase64(gzip.codeUnits);
  }

  static String decode(String string) {
    var bytes = CryptoUtils.base64StringToBytes(string);
    var gzip = new String.fromCharCodes(bytes);
    return js.context.RawDeflate.inflate(gzip);
  }
}
Then I can make this a part of ice as well:
library ice;

import 'dart:async';
import 'dart:collection';
import 'dart:crypto';
import 'dart:html';
import 'dart:json' as JSON;
import 'package:js/js.dart' as js;
import 'package:js/js_wrapping.dart' as jsw;

part 'editor.dart';
part 'store.dart';
part 'gzip.dart';
That seems a fine stopping point for tonight. Up tomorrow: I get back to adding features.


Day #749

No comments:

Post a Comment