DEV Community

Muhammad Ahmad
Muhammad Ahmad

Posted on

Functional Programming in JS: 0x06 - Category Theory - Theme 2

Monads

Monads are tools that help you compose functions.
Like primitive types, monads are structures that can be used as the containers that functors “reach into”. The functors grab the data, do something to it, put it into a new monad, and return it.

There are three monads we’ll focus on:

  • Maybes
  • Promises
  • Lenses

So in addition to arrays (map) and functions (compose), we’ll have five functors (map, compose, maybe, promise and lens). These are just some of the many other functors and monads that are out there.

Maybes

Maybes allow us to gracefully work with data that might be null and to have defaults.

A maybe is a variable that either has some value or it doesn’t. And it doesn’t matter to the caller.

On its own, it might seem like this is not that big a deal.
Everybody knows that nullchecks are easily accomplished with an if-else statement:

if (getUsername() == null) {
    username = 'Anonymous') {
    else {
        username = getUsername();
    }
Enter fullscreen mode Exit fullscreen mode

But with functional programming, we’re breaking away from the procedural, line-by-line way of doing things and instead working with pipelines of functions and data. If we had to break the chain in the middle just to check if the value existed or not, we would have to create temporary variables and write more code. Maybes are just tools to help us keep the logic flowing through the pipeline.

To implement maybes, we’ll first need to create some constructors.

// the Maybe monad constructor, empty for now
var Maybe = function() {};
// the None instance, a wrapper for an object with no value
var None = function() {};
None.prototype = Object.create(Maybe.prototype);
None.prototype.toString = function() {
    return 'None';
};
// now we can write the `none` function
// saves us from having to write `new None()` all the time
var none = function() {
    return new None()
};
// and the Just instance, a wrapper for an object with a value
var Just = function(x) {
    return this.x = x;
};
Just.prototype = Object.create(Maybe.prototype);
Just.prototype.toString = function() {
    return "Just " + this.x;
};
var just = function(x) {
    return new Just(x)
};
Enter fullscreen mode Exit fullscreen mode

Finally, we can write the maybe function. It returns a new function that either returns nothing or a maybe. It is a functor

var maybe = function(m) {
        if (m instanceof None) {
            return m;
        } else if (m instanceof Just) {
            return just(m.x);
        } else {
            throw new TypeError("Error: Just or None expected, " + m.toString() + "
                given.
                ");
            }
        }
Enter fullscreen mode Exit fullscreen mode

And we can also create a functor generator just like we did with arrays.

var maybeOf = function(f) {
    return function(m) {
        if (m instanceof None) {
            return m;
        } else if (m instanceof Just) {
            return just(f(m.x));
        } else {
            throw new TypeError("Error: Just or None expected, " + m.toString() +
                " given.");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So Maybe is a monad, maybe is a functor, and maybeOf returns a functor that is already assigned to a morphism.
We’ll need one more thing before we can move forward. We’ll need to add a method to the Maybe monad object that helps us use it more intuitively:

Maybe.prototype.orElse = function(y) {
    if (this instanceof Just) {
        return this.x;
    } else {
        return y;
    }
}
Enter fullscreen mode Exit fullscreen mode

In its raw form, maybes can be used directly.

maybe(just(123)).x; // Returns 123
maybeOf(plusplus)(just(123)).x; // Returns 124
maybe(plusplus)(none()).orElse('none'); // returns 'none'
Enter fullscreen mode Exit fullscreen mode

Anything that returns a method that is then executed is complicated enough to be begging for trouble. So we can make it a little cleaner by calling on our curry() function.

maybePlusPlus = maybeOf.curry()(plusplus);
maybePlusPlus(just(123)).x; // returns 123
maybePlusPlus(none()).orElse('none'); // returns none
Enter fullscreen mode Exit fullscreen mode

But the real power of maybes will become clear when the dirty business of directly calling the none() and just() functions is abstracted. We’ll do this with an example object User, that uses maybes for the username.

var User = function() {
    this.username = none(); // initially set to `none`
};
User.prototype.setUsername = function(name) {
    this.username = just(str(name)); // it's now a `just
};
User.prototype.getUsernameMaybe = function() {
    var usernameMaybe = maybeOf.curry()(str);
    return usernameMaybe(this.username).orElse('anonymous');
};
var user = new User();
user.getUsernameMaybe(); // Returns 'anonymous'
user.setUsername('Laura');
user.getUsernameMaybe(); // Returns 'Laura'
Enter fullscreen mode Exit fullscreen mode

And now we have a powerful and safe way to define defaults. Keep this User object in mind because we’ll be using it later on in this part.

Promises

The nature of promises is that they remain immune to changing circumstances.
-Frank Underwood, House of Cards

In functional programming, we’re often working with pipelines and data flows: chains of functions where each function produces a data type that is consumed by the next. However, many of these functions are asynchronous: readFile, events, AJAX, and so on.

Instead of using a continuation-passing style and deeply nested callbacks, how can we modify the return types of these functions to indicate the result?
By wrapping them in promises.

Promises are like the functional equivalent of callbacks. Obviously, callbacks are not all that functional because, if more than one function is mutating the same data, then there can be race conditions and bugs. Promises solve that problem.

You should use promises to turn this:

fs.readFile("file.json", function(err, val) {
    if (err) {
        console.error("unable to read file");
    } else {
        try {
            val = JSON.parse(val);
            console.log(val.success);
        } catch (e) {
            console.error("invalid json in file");
        }
    }
});
Enter fullscreen mode Exit fullscreen mode

Into the following code snippet:

fs.readFileAsync("file.json").then(JSON.parse)
    .then(function(val) {
        console.log(val.success);
    })
    .catch(SyntaxError, function(e) {
        console.error("invalid json in file");
    })
    .catch(function(e) {
        console.error("unable to read file")
    });
Enter fullscreen mode Exit fullscreen mode

The preceding code is from the README for bluebird: a full featured Promises/A+ implementation with exceptionally good performance.
Promises/A+ is a specification for implementing promises in JavaScript. Given its current debate within the JavaScript community, we’ll leave the implementations up to the Promises/A+ team, as it is much more complex than maybes.
But here’s a partial implementation:

// the Promise monad
var Promise = require('bluebird');
// the promise functor
var promise = function(fn, receiver) {
    return function() {
        var slice = Array.prototype.slice,
            args = slice.call(arguments, 0, fn.length - 1),
            promise = new Promise();
        args.push(function() {
            var results = slice.call(arguments),
                error = results.shift();
            if (error) promise.reject(error);
            else promise.resolve.apply(promise, results);
        });
        fn.apply(receiver, args);
        return promise;
    };
};
Enter fullscreen mode Exit fullscreen mode

Now we can use the promise() functor to transform functions that take callbacks into functions that return promises.

var files = ['a.json', 'b.json', 'c.json'];
readFileAsync = promise(fs.readFile);
var data = files
    .map(function(f) {
        readFileAsync(f).then(JSON.parse)
    })
    .reduce(function(a, b) {
        return $.extend({}, a, b)
    });
Enter fullscreen mode Exit fullscreen mode

Lenses

Another reason why programmers really like monads is that they make writing libraries very easy. To explore this, let’s extend our User object with more functions for getting and setting values but, instead of using getters and setters, we’ll use lenses.

Lenses are first-class getters and setters. They allow us to not just get and set variables, but also to run functions over it. But instead of mutating the data, they clone and return the new data modified by the function.

They force data to be immutable, which is great for security and consistency as well for libraries. They’re great for elegant code no matter what the application, so long as the performance-hit of introducing additional array copies is not a critical issue.
Before we write the lens() function, let’s look at how it works.

var first = lens(
    function(a) {
        return arr(a)[0];
    }, // get
    function(a, b) {
        return [b].concat(arr(a).slice(1));
    } // set
);
first([1, 2, 3]); // outputs 1
first.set([1, 2, 3], 5); // outputs [5, 2, 3]
function tenTimes(x) {
    return x * 10
}
first.modify(tenTimes, [1, 2, 3]); // outputs [10,2,3]
Enter fullscreen mode Exit fullscreen mode

And here’s how the lens() function works. It returns a function with get, set and mod defined. The lens() function itself is a functor.

var lens = fuction(get, set) {
    var f = function(a) {
        return get(a)
    };
    f.get = function(a) {
        return get(a)
    };
    f.set = set;
    f.mod = function(f, a) {
        return set(a, f(get(a)))
    };
    return f;
};
Enter fullscreen mode Exit fullscreen mode

Let’s try an example. We’ll extend our User object from the previous example.

// userName :: User -> str
var userName = lens(
    function(u) {
        return u.getUsernameMaybe()
    }, // get
    function(u, v) { // set
        u.setUsername(v);
        return u.getUsernameMaybe();
    }
);
var bob = new User();
bob.setUsername('Bob');
userName.get(bob); // returns 'Bob'
userName.set(bob, 'Bobby'); //return 'Bobby'
userName.get(bob); // returns 'Bobby'
userName.mod(strToUpper, bob); // returns 'BOBBY'
strToUpper.compose(userName.set)(bob, 'robert'); // returns 'ROBERT'
userName.get(bob); // returns 'robert'
Enter fullscreen mode Exit fullscreen mode

jQuery is just a monad

If you think all this abstract babble about categories, functors, and monads has no realworld application, think again. jQuery, the popular JavaScript library that provides an enhanced interface for working with HTML is, in-fact, a monadic library

The jQuery object is a monad and its methods are functors. Really, they’re a special type of functor called endofunctors.

Endofunctors are functors that return the same category as the input, that is, F :: X -> X. Each jQuery method takes a jQuery object and returns a jQuery object, which allows methods to be chained, and they will have the type signature jFunc :: jquery obj -> jquery-obj.

$('li').add('p.me-too').css('color', 'red').attr({id:'foo'});

This is also what empowers jQuery’s plugin framework. If the plugin takes a jQuery object as input and returns one as output, then it can be inserted into the chain.

Let’s look at how jQuery was able to implement this.
Monads are the containers that the functors “reach into” to get the data. In this way, the data can be protected and controlled by the library.

jQuery provides access to the underlying data, a wrapped set of HTML elements, via its many methods.

The jQuery object itself is written as the result of an anonymous function call.

var jQuery = (function() {
    var j = function(selector, context) {
        var jq - obj = new j.fn.init(selector, context);
        return jq - obj;
    };
    j.fn = j.prototype = {
        init: function(selector, context) {
            if (!selector) {
                return this;
            }
        }
    };
    j.fn.init.prototype = j.fn;
    return j;
})();
Enter fullscreen mode Exit fullscreen mode

In this highly simplified version of jQuery, it returns a function that defines the j object, which is actually just an enhanced init constructor.

var $ = jQuery(); // the function is returned and assigned to `$`
var x = $('#select-me'); // jQuery object is returned
Enter fullscreen mode Exit fullscreen mode

In the same way that functors lift values out of a container, jQuery wraps the HTML elements and provides access to them as opposed to modifying the HTML elements directly.

jQuery doesn’t advertise this often, but it has its own map() method for lifting the HTML element objects out of the wrapper.

Just like the fmap() method, the elements are lifted, something is done with them, and then they’re placed back into the container. This is how many of jQuery’s commands work in the backend.

$('li').map(function(index, element) {
    // do something to the element
    return element
});
Enter fullscreen mode Exit fullscreen mode

Another library for working with HTML elements, Prototype, does not work like this.

Prototype alters the HTML elements directly via helpers. Consequently, it has not faired as well in the JavaScript community.

Implementing categories

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
Enter fullscreen mode Exit fullscreen mode

The homoMorph() function is fairly complex. It uses a closure (see 0x03 - Types of Functions) 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
});
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

The preceding code is extremely declarative, safe, reliable, and dependable.

Note

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.

Summary

About one in every 2,000 people has a condition known as synesthesia, a neurological phenomenon in which one sensory input bleeds into another. The most common form involves assigning colors with letters. However, there is an even rarer form where sentences and paragraphs are associated with tastes and feelings.

For these people, they don’t read word by word, sentence by sentence. They look at the whole page/document/program and get a sense for how it tastes—not in the mouth but in the mind. Then they put the parts of the text together like the pieces of a puzzle.

This is what it is like to write fully declarative code: code that describes the relationships between values that tells the machine what we want it to compute.

The parts of the program are not instructions in line-by-line order. Synesthetics may be able to do it naturally, but with a little practice anyone can learn how to put the relational puzzle pieces together.

In this part, we looked at several mathematical concepts that apply to functional programming and how they allow us to build relationships between data. Next, we’ll explore recursion and other advanced topics in JavaScript.

Discussion (0)