SiliconPublishing
Published in

SiliconPublishing

Referential Transparency in JavaScript

How pure functions and immutable state can reduce bugs

One thing that drives me nuts in Javascript is the ease with which developers can corrupt state. Anything that is not hidden in a closure is fair game due to Javascript inability to support sealed objects. Hyrum Wright points out in Hyrum’s Law every observable aspect of your object will eventually be consumed, and usually not in the way you intended. What this means for startups as they scale is more bug reports, longer triage, and lots of finger pointing. And no, prefixing an attribute with an underscore doesn’t stop people from using it.

To help eliminate this category of bugs, I’ve been experimenting with referentially transparent methods in Javascript. A function is referentially transparent if it’s a pure function whose parameters are immutable. To achieve this in JavaScript we use immutable-js to create immutable data, and closures to make sure observers can’t introspect our object and mutate its state. Below is a class based approach that uses closures and a weak map to protect private members.

//using closures
var Authorizer = (function() {
var privateMap = new WeakMap();

class Authorizer {
constructor(keyPair) {
var privateProperties = {
keyPair: keyPair
};
privateMap.set(this, privateProperties);
}
sign () {
return window.btoa(privateMap.get(this).keyPair.get('publicKey'));
};
};

return Authorizer;
}());

// now extend that class
var LoginUtils = (function() {

class LoginUtils extends Authorizer {
constructor(user) {
super(user);
}
sign () {
return super.sign();
};
};

return User;

}());

new LoginUtils(Immutable.Map({name: 'Vader', address: 'Death Star', publicKey: 'Sith', privateKey: 'Lord'})).sign()

Wrapping the class in a self executing function provides a closure for the privateMap variable which is not accessible outside the function call, thereby hiding it from observers. The Authorizer.sign method appears to be referentially transparent. This means it can be memoized. Memoization can improve performance in your web and mobile applications. But is it really? It is possible for Authorizer itself to change privateMap as the result of other method calls which would cause future calls to sign to yield a different result. Let’s try another approach.

Authorizer = function (keyPair) {
return {
sign: function () {
return window.btoa(keyPair.get('publicKey'));
}
};
}

LoginUtils = function (user) {
return {
sign: function () {
return Authorizer(user).sign();
}
};
}

LoginUtils(Immutable.Map({name: 'Vader', address: 'Death Star', publicKey: 'Sith', privateKey: 'Lord'})).sign()

We are still taking advantage of closures to keep our user state inaccessible, and there is no mutable internal state variable. But can you still take advantage of extension, accessor methods, and polymorphism? You bet you can. Let’s add a couple functions that can create accessor methods and an inheritance chain.

// this function will add accessor methods to instance
const addProperty = function (instance, label, getter, setter, enumerable = true) {
Object.defineProperty(
instance,
label,
{
get: getter,
set: setter,
enumerable: enumerable
}
);
};
// this function will mixin other functions from base to sub
const mixin = function (base, sub, params = null) {
const objectToExtend = base(params);
// grab instance properties of the base function
const keys = Object.keys(objectToExtend);
keys.forEach((prop) => {
if(!sub.hasOwnProperty(prop)) {
// check if this property uses accessor methods
const accessors = Object.getOwnPropertyDescriptor(objectToExtend, prop);
if(accessors) {
// yep
addProperty(sub, prop, Object.getOwnPropertyDescriptor(objectToExtend, prop).get,
Object.getOwnPropertyDescriptor(objectToExtend, prop).set);
} else {
// nope
sub[prop] = objectToExtend[prop];
}
}
});
return sub;
}

The addProperty method wraps calls to Object.defineProperty to setup our accessor methods. Note that the mixin function doesn’t care about the base object’s prototype. This is by design. Prototypal inheritance in JavaScript is a mine field, especially for more inexperienced developers. The mixin function also ensures we can copy accessor methods (Object.assign can’t so this). In addition using this approach you don’t have to be restricted to a single inheritance chain.

Next let’s reconstruct our objects. We’re also going to add a naive attempt at memoization. Of course our example simply base64 encodes the data, but when implementing a real encryption method this can save a little time.

const Authorizer = function (user) {
let returnObject = {};
addProperty(returnObject, 'signature', function () {
return window.btoa(user.get('publicKey'));
});
addProperty(returnObject, 'publicKey', function () {
return user.get('publicKey');
});
return returnObject;
};

const LoginUtils = function (user) {
const signResult = Authorizer(user).signature;
let returnObject = {
print: function() {
return 'signature: ' + returnObject.signature +
'\n publicKey: ' + returnObject.publicKey +
'\n name: ' + user.get('name') +
'\n address: ' + user.get('address')
}
};
addProperty(returnObject, 'signature', function () {
console.log('User set signResult');
return signResult;
});
mixin(Authorizer, returnObject, user);
return returnObject;
};

const user = Immutable.Map({name: 'Vader', address: 'Death Star', publicKey: 'Sith', privateKey: 'Lord'});
const utils = LoginUtils(user);
window.alert(utils.print());

JSFiddle of the complete code is here. Now you might be thinking the class based approach is less verbose. There is a lot code just to define and instance method and a couple accessor methods. But there’s no prototypes, no new or this keyword, no mutable state, and no inheritance chain to manage. In my opinion this is much easier to reason about, and test. Also, we can memoize calls to sign! We simply call sign when our object is constructed and never worry about it again.

This isn’t a silver bullet by any means, it’s just an experiment. I’m not building any production code like this. But it’s a fun exercise that opens our minds to new, sometimes more efficient ways of solving problems (and Hyrum’s Law is a big problem in JavaScript). At a startup this kind of mindset is worth its weight in gold. If you can solve problems at the lowest possible value stage (when your fingers hit the keyboard) you can save huge amounts of time and money, thereby increasing your odds of success.

Sources
https://medium.com/the-renaissance-developer/concepts-of-functional-programming-in-javascript-6bc84220d2aa
https://medium.com/javascript-scene/master-the-javascript-interview-what-s-the-difference-between-class-prototypal-inheritance-e4cd0a7562e9
https://curiosity-driven.org/private-properties-in-javascript
https://immutable-js.github.io/immutable-js/

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store