8 steps to turn imperative JavaScript class to a functional declarative code

As an example we will take JavaScript code to create event manager adding event listeners and dispatching events. Since JavaScript is a multiparadigm programming language, my intent is to show you options of how the worlds of imperative and declarative codes may be used together so you can make your own decision which features of the language you want to use.

If you want to know more about the difference between declarative and imperative coding, have a look here: Imperative versus declarative code… what’s the difference?

class EventManager {
  construct (eventMap = new Map ()) {
this.eventMap = eventMap;
}
  addEventListener (event, handler) {
if (this.eventMap.has (event)) {
this.eventMap.set (event, this.eventMap.get (event).concat ([handler]));
} else {
this.eventMap.set (event, [handler]);
}
}
  dispatchEvent (event) {
if (this.eventMap.has (event)) {
const handlers = this.eventMap.get (event);
for (const i in handlers) {
handlers [i] ();
}
}
}
}
const EM = new EventManager ();
EM.addEventListner ('hello', function () {
console.log ('hi');
});
EM.dispatchEvent ('hello'); // hi

Our challenge is…

  • Turn these 20 lines of imperative code into 7 lines of 2 expressions of declarative code.
  • Don’t use curly braces {} and IF conditions.
  • Be purely functional, no imperative code.
  • Create only unary functions, functions require exactly one argument.
  • Create pure functions with no external effects, no external data used or modified.
  • Make the functions composable.
  • Everything will be clean, loosely coupled and beautiful.

If you are getting lost in the new terms, consider reading: 6 fundamental terms in functional JavaScript.

Step 1: Replace class by functions

const eventMap = new Map ();
function addEventListener (event, handler) {
if (eventMap.has (event)) {
eventMap.set (event, eventMap.get (event).concat ([handler]));
} else {
eventMap.set (event, [handler]);
}
}
function dispatchEvent (event) {
if (eventMap.has (event)) {
const handlers = this.eventMap.get (event);
for (const i in handlers) {
handlers [i] ();
}
}
}

If you wanted to use this code as your module, you can simple add export at the end of your code:

// ./event-manager.js
export default {addEventListener, dispatchEvent};

And then import it and use it as a singleton:

import * as EM from './event-manager.js';
EM.dispatchEvent ('event');

Modules behave as singletons, when it comes to their inner variables and therefor if you import the module in different places, the inner eventMap variable will remain shared.

Step 2: Use lambda arrow functions

Arrow functions, or lambdas, are easy to use:

const eventMap = new Map ();
const addEventListener = (event, handler) => {
if (eventMap.has (event)) {
eventMap.set (event, eventMap.get (event).concat ([handler]));
} else {
eventMap.set (event, [handler]);
}
}
const dispatchEvent = event => {
if (eventMap.has (event)) {
const handlers = eventMap.get (event);
for (const i in handlers) {
handlers [i] ();
}
}
}

Instead of using function keyword, we now have constants with functions defined through lambdas. Nothing else changes. However, be aware that arrow functions use lexical this and you would not be able to use them inside of the class definition that we had at the beginning.

Step 3: Remove side effects and add returns

Make event map one of the arguments for both of our functions and add returns:

const addEventListener = (event, handler, eventMap) => {
if (eventMap.has (event)) {
return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
} else {
return new Map (eventMap).set (event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
if (eventMap.has (event)) {
const handlers = eventMap.get (event);
for (const i in handlers) {
handlers [i] ();
}
}
return eventMap;
}
const myMap =
addEventListner ('hello', () => console.log ('hi'), new Map ());
dispatchEvent ('hello', myMap); // hi

Now we are on our way to functional purity and composability. Notice that we have also added new Map to return a new map and not create a side effect on the previous.

Step 4: Remove for statement

We will replace for with foreach:

const addEventListener = (event, handler, eventMap) => {
if (eventMap.has (event)) {
return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
} else {
return new Map (eventMap).set (event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
if (eventMap.has (event)) {
eventMap.get (event).forEach (a => a ());
}
return eventMap;
}

Step 5: Apply a bit of binary logic

To correctly apply disjunction || and conjunction && in return statements depends on your undrestanding of return values of involved functions. If you are not sure, try and test your code.

const addEventListener = (event, handler, eventMap) => {
if (eventMap.has (event)) {
return new Map (eventMap).set (event, eventMap.get (event).concat ([handler]));
} else {
return new Map (eventMap).set (event, [handler]);
}
}
const dispatchEvent = (event, eventMap) => {
return (
eventMap.has (event) &&
eventMap.get (event).forEach (a => a ())
) || event;
}

Step 6: Replace if with ternary operator

Ternary operators are actually fun to use even when you are using ternary operator inside of a ternary operators. They are easy to read and understand if you give them a chance.

const addEventListener = (event, handler, eventMap) => {
return eventMap.has (event) ?
new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
new Map (eventMap).set (event, [handler]);
}
const dispatchEvent = (event, eventMap) => {
return (
eventMap.has (event) &&
eventMap.get (event).forEach (a => a ())
) || event;
}

Ternary operators are not for everyone and they can get complicated once you start nesting them. To provide an alternative I have become the developer of NPM package conditional-expression. You can read about it more in my post: How to replace switch and ternaries in functional JavaScript.

Step 7: Now you don’t need any {}

Since arrow function always returns the value of the expression, you don’t need any {} or return statements.

const addEventListener = (event, handler, eventMap) =>
eventMap.has (event) ?
new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
new Map (eventMap).set (event, [handler]);
const dispatchEvent = (event, eventMap) =>
(eventMap.has (event) && eventMap.get (event).forEach (a => a ()))
|| event;

Step 8: Currying

Our last step is currying to turn our functions into unary with only one argument creating higher-order functions. I invite you to read my post on this topic to learn more: JavaScript ES6 curry functions with practical examples. To make your life simpler, you can imagine it as a process of changing your arguments into a series of arrows turning (a, b, c) into 
a => b => c.

const addEventListener = handler => event => eventMap =>
eventMap.has (event) ?
new Map (eventMap).set (event, eventMap.get (event).concat ([handler])) :
new Map (eventMap).set (event, [handler]);
const dispatchEvent = event => eventMap =>
(eventMap.has (event) && eventMap.get (event).forEach (a => a ()))
|| event;

When thinking about order of your arguments, you need to think about your future partial applications and functional compositions. But that is something that you will figure out easily by just trying.

You have many options how to work with your two expressions now:

const log = x => console.log (x) || x;
// call as they are
const myEventMap1 =
addEventListener (() => log ('hi')) ('hello') (new Map ());
dispatchEvent ('hello') (myEventMap1); // hi
// partial application
let myEventMap2 = new Map ();
const onHello = handler =>
myEventMap2 = addEventListener (handler) ('hello') (myEventMap2);
const hello = () => dispatchEvent ('hello') (myEventMap2);
onHello (() => log ('hi'));
hello (); // hi
// composition
const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args)));
const addEventListeners = compose (
log,
addEventListener (() => log ('hey')) ('hello'),
addEventListener (() => log ('hi')) ('hello')
);
const myEventMap3 = addEventListeners (new Map ()); // myEventMap3
dispatchEvent ('hello') (myEventMap3); // hi hey

To be fair, the next step to a functional code purity should be the introduction of functors. However, the concept of functors can be quite complicated for someone just starting with functional programming and so I am intentionally stopping here.

Final thought

Depending on your personal style, skills and need, stop at a step that best suits you. If you just want to improve side effects and testability of your code, stop at step 3. You can decide yourself what programming paradigms best meet your needs. Happy coding.