Saturday, May 11, 2013

TDD with Static Typing in Dart

‹prev | My Chain | next›

Last night, Morgan Nelson, who graciously served as my #pairwithme pair, and I built a browser persistent, ordered HashMap in Dart. The idea is that the projects being stored in the ICE Code Editor need to be ordered so that the most recently worked on projects are first and so that they can be accessed by project title.

The number of projects will be a few dozen at most so an inefficient lookup is not a problem. In other words, I can achieve the desired order by using a List and the lookup by scanning the list. HashMap lookup in Dart is done with the square-bracket operator. Given that I have a projects List of all projects, we made use of the firstWhere() method to find a project given a title:
class Store implements HashMap<String, HashMap> {
  // ...
  HashMap operator [](String key) {
    return projects.
      firstWhere(
        (p) => p['title'] == key,
        orElse: () => null
      );
  }
  // ...
}
By default firstWhere() will throw an exception if no matching item is found, which is where the orElse optional argument comes in handy.

Now that we can lookup projects, we can also create and update them by defining the []= operator:
class Store implements HashMap<String, HashMap> {
  // ...
  HashMap operator [](String key) {
    return projects.
      firstWhere(
        (p) => p['title'] == key,
        orElse: () => null
      );
  }

  void operator []=(String key, Object data) {
    data['title'] = key;

    _updateAtIndex(_indexOfKey(key), data);

    _sync();
  }
  // ...
}
Most of the work of create-or-update is done by the private _updateAtIndex() method. It creates a new record at the front of the list if no matching project exists, otherwise it replaces the old project directly in the list.

The most important bit in that list the _sync() method, which is responsible for persisting the list of projects into localStorage(). It is rather dull, given its importance:
  void _sync() {
    window.localStorage[codeEditor] = JSON.stringify(projects);
  }
Then again dull code is often the best code.

Anyhow, we got that all coded and have a bunch of tests that helped drive the implementation and remain behind to catch regressions. Today I hope to catch regressions.

Next up, I want to satisfy the dart_analyzer, which checks the various declared types to find the following problems:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/store.dart 
file:/home/chris/repos/ice-code-editor/lib/store.dart:18:7: Concrete class Store has unimplemented member(s) 
    # From HashMap:
        bool isEmpty
        Iterable<String> keys
        Iterable<HashMap<dynamic, dynamic>> values
        bool containsKey(String)
        bool containsValue(HashMap<dynamic, dynamic>)
        void addAll(Map<String, HashMap<dynamic, dynamic>>)
        HashMap<dynamic, dynamic> putIfAbsent(String, () -> HashMap<dynamic, dynamic>)
        HashMap<dynamic, dynamic> remove(String)
        void clear()
        void forEach((String, HashMap<dynamic, dynamic>) -> void)
    17:  */
    18: class Store implements HashMap<String, HashMap> {
              ~~~~~
This is dart_analyzer telling me that I have declared the ICE Store class to be a HashMap, but have failed to define a bunch of methods that make a HashMap a HashMap.

This code will run perfectly fine without defining these methods—Dart does not really care. But the dart_analyzer is right: I have declared this as a HashMap and people might actually want to treat it as such.

I will not TDD most of these. Instead, I will delegate most to the projects getter and allow typing to catch problems. That is, if I define the isEmpty getter as:
class Store implements HashMap<String, HashMap> {
  // ...
  bool get isEmpty => projects.asdf;
  // ...
}
Then dart_analyzer will complain:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/store.dart
...
file:/home/chris/repos/ice-code-editor/lib/store.dart:64:32: "asdf" is not a member of List
    63: 
    64:   bool get isEmpty => projects.asdf;
                                       ~~~~
That is something that I can fix with:
class Store implements HashMap<String, HashMap> {
  // ...
  bool get isEmpty => projects.isEmpty;
  // ...
}
While working through these, my TDD by static typing catches a bug when I added a plural value instead of the intended singular:
ile:/home/chris/repos/ice-code-editor/lib/store.dart:68:48: 'Iterable<HashMap<dynamic, dynamic>>' is not assignable to 'HashMap<dynamic, dynamic>'
    67:   bool containsKey(key) => keys.contains(key);
    68:   bool containsValue(value) => values.contains(values);
                                                       ~~~~~~
That aside, this process goes fairly smoothly, leaving me with a fair sampling of the needed HashMap methods:
class Store implements HashMap<String, HashMap> {
   // ...
  int get length => projects.length;
  bool get isEmpty => projects.isEmpty;
  Iterable<String> get keys => projects.map((p)=> p['title']);
  Iterable<HashMap> get values => projects.map((p)=> p);
  bool containsKey(key) => keys.contains(key);
  bool containsValue(value) => values.contains(value);
  void forEach(f) {
    projects.forEach((p)=> f(p['title'], p));
  }
  // ...
}
The dart_analyzer still complains about the destructive methods:
➜  ice-code-editor git:(master) ✗ dart_analyzer lib/store.dart
file:/home/chris/repos/ice-code-editor/lib/store.dart:18:7: Concrete class Store has unimplemented member(s) 
    # From Map:
        HashMap<dynamic, dynamic> putIfAbsent(String, () -> HashMap<dynamic, dynamic>)
        HashMap<dynamic, dynamic> remove(String)
        void clear()
    # From HashMap:
        void addAll(Map<String, HashMap<dynamic, dynamic>>)
    17:  */
    18: class Store implements HashMap<String, HashMap> {
              ~~~~~
But those seem worthy of dropping down to full-blown TDD. Which is what I will do with tonight's #pairwithme pair!


Day #748

No comments:

Post a Comment