📌 Preface
"Best Practises" are very subjective, so your question cannot really be answered in that regard, but I will try to give you some tips nonetheless.
To lead into the rest of my answer I would say one of the most beginner-friendly resources for learning a functional style in javascript is this Medium article (without going into a lot of the nitty-gritty theoretical stuff). By reading this I think you will come to understand a lot of the ideas that come with functional programming.
💁♀️ Paradigm vs. Style
With that out of the way, let us start with a shocker: Javascript is not really a functional programming language. Yep, I said it. Javascript is more a procedural (or object-oriented) language (some newer features have allowed for more functional-ish style but it remains very procedural). But all that ofcourse does not mean you cannot apply a functional style, which I think is more the core of your question?
Presuming I did correctly interpret your question, let me paraphrase to you the first two sentences of the Wikipedia page about FP:
[...] functional programming is a programming paradigm where programs are constructed by applying and composing functions. It is a declarative programming paradigm [...], rather than a sequence of imperative statements [...].
This means that, when writing your programs, you do not tell the computer exactly what to do every step of the way (i.e. for (let i = 0; i < n; i++)
) but rather you chunk a problem into smaller sub-problems, solve those, and compose a function that calls your smaller functions to solve the bigger problem. This leads me to my first tip. Javascript functions come in two forms: procedures, and lambda's. I am not going into the details that differentiate the two, but rather I would like to tell you about lambda's.
💡 Tip 1: Embrace expressions
You seem to know javascript's lambda syntax, since you use the arrow notation (x => y
), but you are using it in a procedural way. What I mean by that, is that you append a block to each lambda, as such:
const doSomething = (arg1, arg2) => { /*steps*/ };
This kind of defeats the purpose of the notation, and is basically another syntax for the procedural functions of JS:
function doSomething (arg1, arg2) { /*steps*/ }
In Javascript, x => y
means a function that takes x and returns y. So a lot of your functions can be simplified. Let's change a couple to make it clear:
const sayHello = (name) => {
return `Hello, ${name}!`;
};
// can be
const sayHello = name => `Hello ${name}!`;
and
const addNums = (x, y) => {
return x + y;
};
// can be
const add = (x, y) => x + y;
💡 Tip 2: Spice up your functions
But then what is the purpose of the lambda notation, and why is your code defeating its purpose? To put it simply: a lambda is a function that takes zero or one argument(s), maybe transforms it, and yields a result. Wikipedia's article about lambda calculus says the same, but with more jargon:
[...] all functions in the lambda calculus are anonymous functions, having no names. They only accept one input variable, with currying used to implement functions with several variables.
You use a lot of functions as procedures (sequence of steps) which you cannot have in FP, and you are using multiple parameter functions which are also disallowed (or rather just do not exist in FP). But, you might ask, how are you supposed to do anything with just one parameter and a single expression body? Let us take a snippet of your code and refactor it into a nice functional style:
// Original
const createButton = (text, func) => {
const button = document.createElement('button');
const buttonText = document.createTextNode(text);
button.appendChild(buttonText);
button.addEventListener('click', () => {
return func();
});
return button;
};
// Functional alternative
// Some setup, notice the currying for multiple arguments.
const text = text => document.createTextNode(text);
const newButton = () => document.createElement("button");
const listen = event => handler => target => {
// Javascript was not meant to be used purely functionally,
// so we have to use a block with several statements to
// mutate the target, and then return it (mutation will never
// happen in functional programming, as you might know).
target.addEventListener(event, handler);
return target;
};
const append = child => target => {
// Same story: we have to return the target after mutating it.
target.append(child);
return target;
};
// Here comes the true power of functional programming: composition
const onClick = listen("click");
const addLabel = label => append(text(label));
const sayHi = () => console.log("Hi!");
const sayBye = () => console.log("Bye.");
const button = label => handler =>
addLabel(label)(
onClick(handler)(
newButton()));
const friendlyButton = button(":)")(sayHi);
const sadButton = button(":(")(sayBye);
// Try it out!
document.body.append(friendlyButton, sadButton);
Ok, giant code sample with weird and maybe hideous things. Let me explain it!
Firstly I would like to say that Javascript is a great language, but the DOM is not the best part to use FP for, because it's an amalgamation of mutation and imperativeness, which are the arch-enemies of FP. But let us do it anyways because it is fun!
The first statement of the refactor goes as follows:
const text = text => document.createTextNode(text);
which is essentially a function that, when called with an argument text
will create a textNode with text
being its text. Now this is a bit of a weird one, because we made a function that takes an argument, and then calls another function with that very argument, so we basically aliased it. Aliasing is the same as storing it in a variable with another name, so why not write it like this:
const text = document.createTextNode;
text
is equal to document.createTextNode
, so text("Hi!")
is equal to document.createTextNode("Hi!")
.
Now JS is a bit weird (and full of side-effects), so we have to specify that the this
of document.createElement
is document
(document.createElement.bind(document)
). The peculiarities of Javascript's this
go far beyond the purpose of this post so if you do not understand what that meant, just wrap it in a function like so: x => y(x)
;
The first real weirdness happens in the listen
function:
const listen = event => handler => target => { /*...*/ };
What? A function that returns a function that returns a function that returns something else?! Yep, that is functional programming baby! But why would we do this? Because it allows for very easy composition. Notice how later on, we defined the function onClick
, which is a composed listen
:
const onClick = listen("click");
// This means the following:
const onClick = handler => target => target.addEventListener("click", handler);
So we made a function that always adds a click
eventlistener to a target. We can take this a step further:
// Let's make a function that makes an element yell "OUCH!" on click!
const ouch = onClick(() => console.log("OUCH!"));
// This means the following:
const ouch = target => target.addEventListener("click", () => console.log("OUCH!"));
// Try it out!
const boxingDummy = ouch(addLabel("Don't hit me!")(newButton()));
document.body.append(boxingDummy);
I hope that made it clear why you would write programs in such a weird (coming from procedural programming) way. I would link you the wiki article but that is just unintelligble jibberish to me too.
You seem to really rely on the imperativeness of Javascript to perform tasks in a set order, which does not fit well in a functional style -- most functional languages do not even allow it. The great functional programming evangelists have thought about this long and hard, and showed us how you do not need loops, if's or sequences to create complete and functional (in the literal sense) programs. Taking, for example, another function you wrote:
const view = () => {
const component = document.createElement('div');
// create a button element that has a callback function that will append the returned value to the DOM
component.appendChild(createButton('Hello', () => {
return component.appendChild( createText( sayHello('Kristian') ) );
}));
component.appendChild(createButton('Add Numbers', () => {
return component.appendChild( createText( addNums(3, 7) ) );
}));
component.appendChild( createButton('Log', simpleLog) );
component.appendChild(createButton('Add to array', () => {
console.log( `Original array: [${myList}] - New array: [${addToArray(4)}]`);
}));
return component;
};
Pardon the joke, but this looks really WET... (as in, the opposite of DRY). You call component.appendChild(createButton(/*...*/))
a lot of times. Apart from that being a bit of a code smell, it also is a really great place to put in some FP magic. Analysing the abstract meaning of your code, let me write that out in a more functional style:
// Remember our previous functions?
// I made them nice and generic so they are
// reusable here!
const appendButton = label => action => append(button(label)(action));
const add = x => y => x + y;
const addFive = add(5);
const saySix = () => console.log(addFive(1));
const fold = (...functions) => value => functions.reduce(
(result, fun) => fun(result),
value);
const component = document.createElement("div");
const view = fold(
appendButton("Greet")(sayHi),
appendButton("What's five plus one?")(saySix)
/* etc... */);
// Try it out!
document.body.append(view(component));
BAM! No blocks, no loops, no if's! Instead of a sequence of statements, we use a fold to abstract away a lot of the DOM boilerplate. Higher-order functions such as fold(left) are very important functions to keep in your arsenal. I suggest reading up on it a bit, if the article isn't too crytographic (there's a joke that says functional programmers tend to lose their ability to explain FP as soon as they understand, and I think that applies perfectly on most wiki pages).
Well that should be about it. To summarise:
- Javascript DOM isn't the best place to apply FP (in my humble opinion), but go ahead and break the rules!
- Don't get RSI:
(x) => { return y; }
is the same as x => y
.
- Procedures are so 1970; compose a function from functions to solve your problems (and most importantly: be able to reason about your solution).
- Currying and higher-order functions are a functional programmer's best friends, conquer the world with them.
function name(parameters)
byconst name = (parameters) =>
. What's the point of that? It's the "new" thing? \$\endgroup\$