More thoughts on the LOM and Javascript...

Learn about building and using Max for Live devices.
Post Reply
TomSwirly
Posts: 109
Joined: Wed Apr 30, 2008 7:55 pm

More thoughts on the LOM and Javascript...

Post by TomSwirly » Tue Jan 19, 2010 7:56 pm

So I've been pondering the LOM's Javascript implementation for a bit now that I'm starting to understand it as part of writing a LOM tree walker.

First, I'd like to thank the development team for including it at all! It's really a big deal to have access to this without having to go out into Max world with messages (which is like doing surgery through a keyhole).

My guides.

When I write interfaces, I'm always guided by the principle of least astonishment - "things should behave as expected" and by Scott Meyer's classic on non-friend, non-member functions. In other words, I want as much uniformity and as few side effects as possible.

LiveAPI.path is astonishing.

Let's start with with the LiveAPI class itself. It contains a key piece of state, LiveAPI.path, and other member values like LiveApi.type will change value if you assign to LiveAPI.path.

This violates least astonishment. It's perfectly reasonable to expect changing a variable to change the state of the visible universe, for example turning on a flag turns on a switch on the UI. It's not reasonable to expect changing a variable to change other variables. LiveAPI.path should be a method, LiveAPI.SetPath(). I love using member assignment instead of methods - I do it constantly in my code - but don't use it to mask a significant side-effect.

LiveAPI.path is unnecessarily stateful.

But we can even jump up a higher level and say, "Why is the path part of the LiveAPI's state at all?" This makes code that passes around LiveAPI instances inherently risky - because the path might get changed by someone else.

And it adds to the insecurity of the programmer. For example, I return api.children from a function. I wondered whether this list might be changed later if I change api.path, so I almost wrote code to copy that list. Side-effects are bad!

Suppose instead that all these members were methods that took a path parameter? Then all the state in LiveAPI would reflect "the real world" (settings on the Live sequencer), and so there would be no local state at all on an instance of LiveAPI. In fact, you could make LiveAPI a singleton.

What about callbacks?

But wait, there are the callbacks! Well, we can pull the same trick again - instead of having callbacks stored into the instance of LiveAPI, we call a method on LiveAPI to register the callback. This has the advantage that we can create the callback exactly when we need it, and use a closure.

So each "method" on LiveAPI would actually be a static function!

Weirdness in LiveAPI.children?

There are also other strangenesses to the LiveAPI. For example, LiveAPI.children behaves in an unexpected fashion; if the path points to something of type 'tuple' or 'list', LiveAPI.children contains the two element list ['No', 'children'] and you need to back up one level, use getcount(), and iterate through integer indices. Messy! The children should always be a list of valid names that can be used to extend the path, and if that happens to be [0, 1, 2...] then that's great.

Are paths lists or strings?

There's also no need to accept both space separated strings and lists for paths. In Javascript it's simpler to only take lists and let people say path.split(' ') if that's what they want. Again, the more specificity, the simpler it is for the programmer to learn. Unnecessary flexibility introduces uncertainty (as in "if I set LiveAPI.path with a space-separated string/list of strings and then read it, do I get a list or a string?").

Code samples.

I've written some code on top of the regular LiveAPI to deal with this.

Code: Select all

var Live = new Object();  // From the file live/live.js

Live.API = new Object();  // From the file live/api.js
Live.API.instance = new LiveAPI(this.patcher);

// From the file live/get_children.js.

// These are types whose children have integer indexes, rather than using the      
// names in api.children.                                                          
Live.API.indexed_types = {'list': 1, 'tuple': 1 };

// Get a list of the child names at that path location.
// If that path has no children, it returns the empty list;
// if that path is an indexed type, it returns a list of the indices from 0 to 
Live.API.GetChildren = function(path) {
  var api = Live.API.instance;
  if (!path.length) {
    // Special case for the root.                                                  
    api.path = path;
    return api.children;
  }

  // Move to the parent path and count the children.                               
  api.path = path.slice(0, path.length - 1);
  var child_count = api.getcount(path[path.length - 1]);
  if (!child_count) {
    post(Print(path), 'has no children\n');
    return [];
  }

  api.path = path;
  if (Live.API.indexed_types[api.type]) {
    var result = [];
    for (var i = 0; i < child_count; ++i)
      result.push(i);
    return result;
  } else {
    return api.children;
  }
};
Some notes on the code.

Reading this, I'm sure some of you are finding this code strange, so let me explain.

I'm creating the singleton object variable Live entirely as a namespace to store constants and functions. Live.API is also a singleton object used as a namespace. Live.API.indexed_types is a dictionary and Live.API.GetChildren is a function that takes a path, expressed as a list, and returns a list of names of children.

This style is very convenient for my coding style, where I have one major function or class per file, and large classes will be divided amongst separate files. And I think it looks really slick, too, to write Live.API.GetChildren(path) :D

Post Reply