Fork me on GitHub
isolate.js via AST analysis
interactivate
Recent changes in SDK
nodeconf 2012
Write logic, not mechanics
protocol based polymorphism

Not a long time ago I learned about clojure’s polymorphism constructs and protocols. I was so inspired by a porwer and flexibility of protocol based polymorphism that I decide to prototyped it for JS. In this post I will try to give you a taste of protocols and maybe even motivate you give them a try.

Rationale

In programing we usually write and consume various abstractions. Typically in OOP languages abstractions are defined via (class / object) interfaces and have a nasty expression problems. Imagine that you have A and B sets of abstractions and sets of implementations of those abstractions. A should be able to work with B’s abstractions, and vice versa without modifications of the original code. While it may not sound as a problem at first, it usually is in practice. Sometimes A can’t use B, either because they were not designed to work with each other as they were written by a different authors or because one is newer than the other. Either way such cases require code changes, which may be difficult because code is old, or complicated or has a license restrictions and there could be millions of other reasons. Any code hits these issue in some form and it’s just matter of time. When that happens we’re left only with a few possible solutions:

Feature detection

Typically this is a code that is written not in terms of abstractions, but entities, that do runtime branching by “feature detection”. Which may be a type (if (value instanceof Type)) or shape (if (value && typeof(value.length) === 'number')) based. This not only makes code harder to read & reason about, but it also closed. In other words every new abstraction will require rewrite of those entities, in order to accumulate more conditions and branches.

Wrappers

Typically this means that entities implementing abstraction A need to be wrapped by a “glue code” implementing abstraction from B and vice versa, if consumption is bidirectional. Unfortunately this introduces lot’s of incidental complexity as wrappers ruin identity & don’t compose (every new abstraction requires wrappers for all existing ones and vice versa). Finally problem and required changes grow progressively with a number of abstractions used.

Monkey patching

Typically this means that implementation of A abstraction is patched with a “glue code” implementing support for B abstraction. This still introduces complexity by ruining namespacing (different abstractions may have conflicting names). Again problem gets worth with an amount of code. Also in some languages this is not even possible.

Note: For more details I would recommend watching a “A quick overview of clojure protocols” by Stuart Halloway.

Protocols

In Clojure polymorphism is achieved using protocols. They provide a powerful way for decoupling abstraction interface definition from an actual implementation per type, without risks of interference with other libraries. Protocols allow to add polymorphic behavior to things that already exist without changing them. I’ll go into more details on protocols, but for code examples I will use my JS prototype implementation instead of clojure code.

There are several motivations for JS protocol library:

  • Provide a high-performance, dynamic polymorphism construct as an alternative to an existing object inheritance that does not provides any mechanics for dealing with name conflicts.

  • Provide the best parts of interfaces:

    • Specification only, no implementation

    • Single type can implement multiple protocols

  • Allow independent extension of types, protocols and implementations of protocols on types, by different parties.

Define protocol

A protocol is a named set of functions and their signatures defined by calling protocol function:

/*jshint asi:true */
// module: ./event-protocol

var protocol = require('protocol/core').protocol

// Defining a protocol for working with an event listeners / emitters.
module.exports = protocol({
  // Function on takes event `target` object implementing
  // `Event` protocol as first argument, event `type` string
  // as second argument and `listener` function as a third
  // argument. Optionally forth boolean argument can be
  // specified to use a capture. Function allows registration
  // of event `listeners` on the event `target` for the given
  // event `type`.
  on: [ protocol, String, Function, [ Boolean ] ],

  // Function allows registration of single shot event `listener`
  // on the event `target` of the given event `type`.
  once: [ protocol, 'type', 'listener', [ 'capture=false' ] ],

  // Unregisters event `listener` of the given `type` from the given
  // event `target` (implementing this protocol) with a given `capture`
  // face. Optional `capture` argument falls back to `false`.
  off: [ protocol, 'type', 'listener', [ 'capture=false'] ],

  // Emits given `event` for the listeners of the given event `type`
  // of the given event `target` (implementing this protocol) with a given
  // `capture` face. Optional `capture` argument falls back to `false`.
  emit: [ protocol, 'type', 'event', [ 'capture=false' ] ]
})
  • No implementations are provided
  • Code above returns a set of polymorphic functions and a protocol object
  • Resulting functions dispatch on the argument with an index denoted in a signature via special protocol element.
  • Other array elements of the signature represent interface definition, and does not yet carry any special meaning. (You could use functions to highlight types or strings to highlight names or come up with something more creative).
  • Protocol implementations can be provided at any time from any scope that has access to defined protocol.

Implement protocol

Defined protocols can be implemented per type bases. Since everything in JS is an Object protocol implementation for Object type can be though as a default, since all values will automatically support protocol via that implementation:

/*jshint asi:true */
// module: ./event-object

var Event = require('./event-protocol'), on = Event.on

// Weak registry of listener maps associated
// to event targets.
var map = WeakMap()

// Returns listeners of the given event `target`
// for the given `type` with a given `capture` face.
function getListeners(target, type, capture) {
  // If there is no listeners map associated with
  // this target then create one.
  if (!map.has(target)) map.set(target, Object.create(null))

  var listeners = map.get(target)
  // prefix event type with a capture face flag.
  var address = (capture ? '!' : '-') + type
  // If there is no listeners array for the given type & capture
  // face than create one and return.
  return listeners[address] || (listeners[address] = [])
}

Event(Object, {
  on: function(target, type, listener, capture) {
    var listeners = getListeners(target, type, capture)
    // Add listener if not registered yet.
    if (!~listeners.indexOf(listener)) listeners.push(listener)
  },
  once: function(target, type, listener, capture) {
    on(target, type, listener, capture)
    on(target, type, function cleanup() {
      off(target, type, listener, capture)
    }, capture)
  },
  off: function(target, type, listener, capture) {
    var listeners = getListeners(target, type, capture)
    var index = listeners.indexOf(listener)
    // Remove listener if registered.
    if (~index) listeners.splice(index, 1)
  },
  emit: function(target, type, event, capture) {
    var listeners = getListeners(target, type, capture).slice()
    // TODO: Exception handling
    while (listeners.length) listeners.shift().call(target, event)
  }
})

Note: Implementing protocol for Object type is not a requirement.

Extend existing types

Existing types (prototypes or constructors / classes) may be extended to implement certain protocol by providing type specific implementation. For example our protocol functions would work with node.js’s EventEmitter objects, but in a funny way. Listeners registered by a standard API won’t be called when emitting events with protocol function and vice versa. To fix that we can implement our protocol for the EventEmitter type:

/*jshint asi:true */
// module: ./event-emitter

var EventProtocol = require('./event-protocol')
var EventEmitter = require('events').EventEmitter

EventProtocol(EventEmitter, {
  on: function(target, type, listener, capture) {
    target.on(type, listener)
  },
  once: function(target, type, listener, capture) {
    target.once(type, listener)
  },
  off: function(target, type, listener, capture) {
    target.removeListener(target, type)
  },
  emit: function(target, type, event, capture) {
    target.emit(type, event)
  }
})

Now this is cool, we managed to add support for our event abstraction to a type that was not designed to work with it without changing a single line of code. But this is just a tip of the iceberg, we could implement this protocol for more types, let’s try to do it for DOM elements:

/*jshint asi:true latedef: true */
// module: ./event-dom

var Event = require('./event-protocol')

Event(Element, {
  on: function(target, type, listener, capture) {
    target.addEventListener(type, listener, capture)
  },
  off: function(target, type, listener, capture) {
    target.removeListener(type, listener, capture)
  },
  emit: function(target, type, option, capture) {
    // Note: This is simplified implementation for demo purposes.
    var document = target.ownerDocument
    var event = document.createEvent('UIEvents')
    event.initUIEvent(type, option.bubbles, option.cancellable,
                      document.defaultView, 1)
    event.data = option.data
    target.dispatchEvent(event)
  }
})

Think of all the different JS frameworks (Backbone, YUI, Three.js, InfoVis, Raphaël, Moo Tools, …) that have their own flavored API for working with events, you could easily extend them to support our event protocol and make their abstractions interchangeable through the rest of the codebase (that makes use of protocols) without original code changes.

Multiple protocols

All the examples above showed how support for a given protocol may be added to a different types, but it’s not only that, any type may be extended to implement multiple protocols with absolutely no risks of naming conflicts. Here is pretty dummy, but still an example illustrating this point:

/*jshint asi:true latedef: true */
// module: ./installable

// Protocol for working with installable application components.
var Installable = protocol({
  // Installs given `component` implementing this protocol. Takes optional
  // configuration options.
  install: [ protocol, [ 'options:Object' ] ],
  // Uninstall given `component` implementing this protocol.
  uninstall: [ protocol ],
  // Activate given `component` implementing this protocol.
  on: [ protocol ],
  // Disable given `component` implementing this protocol.
  off: [ protocol ]
})

Installable(Object, {
  install: function(component, options) {
    // Implementation details...
  },
  uninstall: function(component, options) {
    // Implementation details...
  },
  on: function(component) {
    component.enabled = true
  },
  off: function(component) {
    component.enabled = false
  }
})

module.exports = Installable

Note: That even though both Event and Installable protocols define functions on and off. Also Object implements both still protocols, but there no conflicts arise and functions defined by both protocols can be used without any issues!

Summary

I hope you find this interesting & I’m looking forward to your feedback. All the code examples from this post can be found in the project repository. At the moment library is tested and can be used on node.js & browser, also there are no reasons why it would not work in other JS environments.

I personally think that protocols are much better feet for a JS language than redundant classes and I really wish ES.next was considering them instead!

JavaScript JS Documentation: JS Array some, JavaScript Array some, JS Array .some, JavaScript Array .some
(clojurescripting :intro)
namespaces
JS Guards
Packageless modules
Addons in multi process future
Yet another take on inheritance
Shareable private properties
Evolving VS Adjusting
oh my zsh
Git status in bash prompt
CommonJS based Github library
Taskhub
Gist plugin for Bespin
Reboot
google pages is dead
narwzilla
JSDocs
bespin - JavaScript Server
bespin chromend
Google App Engine + Helma = geekcloud
Bespin to Helma
bespin multibackend mockup
Adjectives | Ubiquity + Bugzilla love
Some Mock-up around Ubiquity
Mozshell
Ubiquity command Say
ubiquity command dictionary
Picasa Photo Viewer (Linux port) - Updated
Ubiquity command for JIRA & Crucible
Picasa Photo Viewer (Linux port)
VirtualBox
KeyZilla 0.1
XUL Development