It's about time we formally defined category theory as JavaScript objects. Categories are objects (types) and morphisms (functions that only work on those types). It's an extremely high-level, totally-declarative way to program, but it ensures that the code is extremely safe and reliable—perfect for APIs and libraries that are worried about concurrency and type safety.
First, we'll need a function that helps us create morphisms. We'll call it homoMorph()
because they'll be homomorphisms. It will return a function that expects a function to be passed in and produces the composition of it, based on the inputs. The inputs are the types that the morphism accepts as input and gives as output. Just like our type signatures, that is, // morph :: num -> num -> [num]
, only the last one is the output.
var homoMorph = function( /* input1, input2,..., inputN, output */ ) { var before = checkTypes(arrayOf(func)(Array.prototype.slice.call(arguments, 0, arguments.length-1))); var after = func(arguments[arguments.length-1]) return function(middle) { return function(args) { return after(middle.apply(this, before([].slice.apply(arguments)))); } } } // now we don't need to add type signature comments // because now they're built right into the function declaration add = homoMorph(num, num, num)(function(a,b){return a+b}) add(12,24); // returns 36 add('a', 'b'); // throws error homoMorph(num, num, num)(function(a,b){ return a+b; })(18, 24); // returns 42
The homoMorph()
function is fairly complex. It uses a closure (see , Fundamentals of Functional Programming) to return a function that accepts a function and checks its input and output values for type safety. And for that, it relies on a helper function: checkTypes
, which is defined as follows:
var checkTypes = function( typeSafeties ) { arrayOf(func)(arr(typeSafeties)); var argLength = typeSafeties.length; return function(args) { arr(args); if (args.length != argLength) { throw new TypeError('Expected '+ argLength + ' arguments'); } var results = []; for (var i=0; i<argLength; i++) { results[i] = typeSafeties[i](args[i]); } return results; } }
Now let's formally define some homomorphisms.
var lensHM = homoMorph(func, func, func)(lens); var userNameHM = lensHM( function (u) {return u.getUsernameMaybe()}, // get function (u, v) { // set u.setUsername(v); return u.getUsernameMaybe(); } ) var strToUpperCase = homoMorph(str, str)(function(s) { return s.toUpperCase(); }); var morphFirstLetter = homoMorph(func, str, str)(function(f, s) { return f(s[0]).concat(s.slice(1)); }); var capFirstLetter = homoMorph(str, str)(function(s) { return morphFirstLetter(strToUpperCase, s) });
Finally, we can bring it on home. The following example includes function composition, lenses, homomorphisms, and more.
// homomorphic lenses var bill = new User(); userNameHM.set(bill, 'William'); // Returns: 'William' userNameHM.get(bill); // Returns: 'William' // compose var capatolizedUsername = fcompose(capFirstLetter,userNameHM.get); capatolizedUsername(bill, 'bill'); // Returns: 'Bill' // it's a good idea to use homoMorph on .set and .get too var getUserName = homoMorph(obj, str)(userNameHM.get); var setUserName = homoMorph(obj, str, str)(userNameHM.set); getUserName(bill); // Returns: 'Bill' setUserName(bill, 'Billy'); // Returns: 'Billy' // now we can rewrite capatolizeUsername with the new setter capatolizedUsername = fcompose(capFirstLetter, setUserName); capatolizedUsername(bill, 'will'); // Returns: 'Will' getUserName(bill); // Returns: 'will'
The preceding code is extremely declarative, safe, reliable, and dependable.
What does it mean for code to be declarative? In imperative programming, we write sequences of instructions that tell the machine how to do what we want. In functional programming, we describe relationships between values that tell the machine what we want it to compute, and the machine figures out the instruction sequences to make it happen. Functional programming is declarative.
Entire libraries and APIs can be constructed this way that allow programmers to write code freely without worrying about concurrency and type safety because those worries are handled in the backend.