Sunday, March 09, 2014

KnockoutJS implicit subscriber via ko.computed: Elegant, yes. Readable, maybe?

I have seen code that looks like this with KnockoutJS a few times, using a ko.computed as a substitute for ko.subscribe on multiple observables:

this.x = ko.observable();
this.y = ko.observable();
this.updated = ko.observable(false);
this.tracker = ko.computed(function() {
  this.x();
  this.y();
  this.updated(true);
}, this);

The tracker function is equivalent to
var callback = function() {
  this.updated(true);
});

this.x.subscribe(callback, this);
this.y.subscribe(callback, this);

The pattern with ko.computed is compact and elegant. A single function tracks multiple observables, and fires an event when one of the tracked observables changes.

My challenge with this pattern is the readability, though.  A non-expert KnockoutJS user may have a hard time figuring out what's going on because the intent is not obvious from the code.  The pattern is exactly the opposite of basic ko.observable usage.  Simple cases like "fullName = firstName + lastName" use the tracked observables to calculate and return an output, and have no other side effect.  In this case, the tracked observables' values aren't used, there is no output, and there are side effects.

On the other hand, a KO expert who understands how ko.computed tracks observables might find this pattern is more readable than the alternative.

So I'm thinking, it's ok to use this pattern, but with some conventions to help prevent beginners from getting too confused.  Some thoughts:

  • Use a naming convention to distinguish this ko.computed pattern from others.  For example, onSomethingChanged or trackSomething.  This way the name sounds like an event listener. 
  • Don't mix the ko.computed patterns - either return a value or have a side effect, but not both
  • Within the ko.computed body, make a clear separation between setting up observable tracking and the action to fire when something changes.  
  • Consider separating the code comments - this is one of those times when code comments are helpful to explain something that might not be obvious from the code itself.
For example:

this.onInputChanged = ko.computed(function() {
  var trackedObservables = [this.x(), this.y()];
  // action when one of trackedObservables changes
  this.updated(true);
}, this);


KO documentation on computeds: http://knockoutjs.com/documentation/computedObservables.html

Wednesday, January 15, 2014

knockout/pager - possible solution for DOM teardown

Possible solution to DOM leakage issue with PagerJS as I observed previously (Knockout is great, not sure about pager):

Add an afterHide event handler to tear down DOM after you leave for another page.  This handler can wipe the DOM from the template, triggering any cleanup registered with ko.utils.domNodeDisposal (see this post on StackOverflow).

Also, by clearing the reference to the view model within the Page object, the view model may be garbage-collected.   This should work for both lazy and non-lazy bindings: for lazy bindings, only the Page object would have a reference to the view model.  For non-lazy, someone else passed a reference to an existing view model so the view model will hang around.

You could attach this callback globally or within individual page bindings.

var afterHideCallback = function afterHideCallback(event) {
  var elementToDestroy = event.page.element;
                     
  // clear out reference from the node with page binding so it can be garbage collected
  event.page.ctx = {};
                                  
  // shut down and wipe DOM from page to be hidden                     
  $(elementToDestroy).children().each(function destroyChildElement() {
    ko.removeNode(this);
  });
};

// attach this event globally or in individual "page: {afterHide: ...}" bindings
pager.afterHide.add(afterHideCallback);

Monday, January 06, 2014

Time intervals and other ranges should be half-open

It is a good practice to treat time intervals as half-open inequalities: start <= x < end. Note the asymmetry.  This is how the Joda-Time API implements time intervals, and also how the SQL "overlaps" keyword works.  Also note that SQL "between" does not behave the same way.

The main reason this is important is to allow adjacent time intervals like 10:00-11:00 and 11:00-12:00, such that the instant of 11:00:00 falls in the second interval, but not the first.

It is a mistake to try turning the intervals into 10:00-10:59, etc. An instant like 10:59:01 would fall through the cracks between intervals, and end minus start would be 59 minutes rather than an hour.

With date or timestamp ranges, the same logic applies.  But there's a key difference: while end-users tend to think of time ranges intuitively as half-open, they often tend to think of date ranges as closed.  That is, users expect 10:00-11:00 and 11:00-12:00 to be adjacent and non-overlapping even though 11:00 is the end of one range and the start of another.  For dates, though, users tend to think of ranges like Jan 1-Dec 31 inclusive of the last day of the month/year.

So what to do?  There are two workable solutions.  One is to apply the same half-open inequality as before, adding a day to the user-specified end date.  So you would have something like 1/1/2013 <= x < 1/1/2014. Another is to truncate the input under test: 1/1/2013 <= trunc(x) <= 12/31/2013.  This gives the same results, because any instant up to and excluding 1/1/2014 will be included in the range.  The first approach is better because you don't have to remember to strip the time portion off of x before testing.

A wrong answer is to do something like 1/1/2013@midnight <= x <= 12/31/2013@23:59:59.  This assumes you know the exact precision of x, and there's a risk of a moment of time falling through the cracks again.  It's also just icky.

Integer ranges where the input under test is a decimal behave like dates and times, with the same options. Currency is a good example: you can use half-open ranges such that $100-200 and $200-300 are adjacent and non-overlapping and $200.00 falls in only the second range.  You could also have the user specify closed ranges like $100-199 and $200-299 and do the same thing with floor(x) or start <= x <= end + 1.  What you can't do is let $199.01 fall between the two adjacent intervals or let $200.00 match both ranges.

Incidentally, it seems like even when we're dealing with pure integer ranges, the common practice in Java is to use half-open intervals for things like substrings, and Python does the same for array slicing.  This seems to be the best practice for programming in general, for readability and minimizing chance of mistakes (see http://stackoverflow.com/questions/8441749/representing-intervals-or-ranges).