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!