Signals operate as a simple way of decoupling dependencies within a project, by allowing caller-callee relationships through an interface that makes both parties anonymous. Assuming a shared signals object is provided, the receiver registers itself on that object:
signals.output = function(text){ alert(text) };
And the sender uses the registered channel to remotely execute that function:
signals.output('Hello');
Signals are the functional equivalent of object-oriented inversion of control, a technique that allows users to configure the behavior of third party code without having to modify it. This is done by removing any explicit dependencies of the third party code on specific behavior units, such as “output a piece of text”, and injecting those dependencies back from the outside as an object or set of objects which hide the actual implementation of those behavior units. Basically, we’re replacing:
function frobnicate(a,b) {
foo(a);
bar(b);
alert('Success');
}
frobnicate(1,2); // Can't prevent the alert box from appearing!
With the slightly longer but easily configured:
function frobnicate(a,b,output) {
foo(a);
bar(b);
output('Success');
}
frobnicate(1,2,function(t){alert(t)}); // Original behavior
frobnicate(1,2,function(){}); // Muted function
frobnicate(1,2,function(t){console.debug(t)}); // To firebug console
Since a given piece of code might depend on several distinct behavior units, I use a record to transmit all that behavior as a single argument. This results in the classic “configure my library with your options object” that can be found, among other places, in jQuery.
This simple approach causes a small number of difficulties:
- If I want to use a slightly different version of a signals object for another part of the program, I have to manually create a copy of the object and change the copy (basically the equivalent of a pure functional object mutation).
- In some situations, I might want to handle several callbacks for a single signal. The current approach only lets me define a single function for a given signal.
- Some functions of the signal set (such as sending a form through AJAX) might rely on other functions of the signal set (display an error message) to handle their own behavior unit dependencies, and I would like those functions to automatically have access to the signal set they belong to, dynamically.
This leads me to a subtly different implementation of signals:
signals = (function(){ s = function() { this._c = s; }; s.prototype.channel = function(c) { var h = [], s = function() { for (var k in h) if (h[k]) h[k].apply(this,arguments); }; s.bind = function(f) { h.push(f); return h.length-1; }; s.unbind = function(f) { h[k] = null; }; return this.set(c,s); }; s.prototype.set = function(n,v){ var i = function(){ this._c = i; }; i.prototype = new this._c(); i.prototype[n] = v; return new i(); }; return s; })();
This small class encapsulates pure functional mutation semantics by means of its set function:
var signals = new signals();
var initial = signals.set('xxx',100);
var final = initial.set('xxx',200);
console.log(initial.xxx + ' ' + final.xxx); // Outputs '100 200'
This small piece of behavior is in itself quite helpful, but it gets better: if a function is added to the object, it remains there but is always executed within the context of the current object and therefore has access to its actual values.
var signals = (new signals()).set('show',function(){console.log(this.xxx)});
var initial = signals.set('xxx',100);
var final = initial.set('xxx',200);
initial.show(); // Displays 100
final.show(); // Displays 200
Last but not least, it’s possible to create a full communication channel that can be connected to several receivers and forwards its arguments to all receivers.All receivers are called with the signals object as their context, which lets them access it and behave accordingly.
var unreadMessages = 0;
var signals = (new signals()).channel('setUnread');
// Update the number of unread messages, notify user if they have
// new messages.
signals.setUnread.bind(function(unread){
if(unreadMessages < unread) this.notice('You have new messages!');
unreadMessages = unread;
});
// Update all places that display the number of unread messages
signals.setUnread.bind(function(unread){
$('.unread').html('Messages'+(unread > 0 ? ' ('+unread+')' : ''));
});
// When at page scope, notices are printed by growling
var global = signals.set('notice',growl);
global.setUnread(10);
// When inside a smaller scope, such as a component, display notices in
// a dedicated location
var local = signals.set('notice',function(arg){$display.html(arg)});
local.setUnread(15);
Hi. I'm Victor Nicollet,
Recent Comments