JavaScript is an amazing programming language that rules the internet. No modern web application is done without it. JavaScript is a multiparadigmatic language that supports both object-oriented and functional approach to programming.
Functions to rule everything
Functional programming is based on the use of functions and functors. Since ES6, JavaScript offers arrow functions for concise lambda expressions:
1const myFunction = value => 'I am ' + value;23myFunction('a turtle'); // => 'I am a turtle';
Which is equivalent to:
1const myFunction = function (value) {2const result = "I am " + value;3return result;4};56myFunction('a turtle'); // => 'I am a turtle';
To follow functional programming practices, you should understand pure functions, higher-order functions, partial application, and function composition.
Pure functions
A pure function is a function in which output depends only on its input without any side effects. Side effect is any change of variables outside of the function's scope (in functional programming that is called mutation). Side effect is also any reading or writing into IO like console log, file reading, database updating and so on.
In functional programming, side effects are managed through monads. Monads make working with side effects more explicit and safe while ensuring general programming best practices.
Side effects, like reading variables outside of functional scope, may not feel particular dangerous to you. Code like this is quite common:
1let tortoises = 'Leonardo';23const addDonatelo = function () {4tortoises = tortoises + ' and Donatelo';5};67addDonatelo(); // => tortoises is 'Leonardo and Donatelo'8tortoises = tortoises.toUpperCase();9addDonatelo(); // => tortoises is 'LEONARDO AND DONATELO and Donatelo
addDonatelo
in the example depends on tortoises
variable which is outside of its scope. The danger lies in the fact that any part of the code can change the value of counter, not just your function. That means that it is impossible to know the effect of addDonatelo
without knowing the current value of tortoises
. At the same time addDonatelo
is also harder to test because you have to mock all your dependencies for any unit test of a function. In a real application such dependencies are often very complicated and lead to unpredictable code that is hard to test.
Functional code is much easier to reason about because pure functions are dependent only on their input.
1const addDonatelo = tortoises => tortoises + ' and Donatelo';2addDonatelo('Leonardo'); // => 'Leonardo and Donatelo'3addDonatelo('Leonardo'); // => 'Leonardo and Donatelo'
addDonatelo
in the functional example is a pure function that depends only on its input. You can always easily say what it's output is going to be.
Functions in a functional programming are declarative lambda expressions that represent a mapping from their inputs to its outputs. That is a very important principle.
JavaScript itself is quite unpredictable because some of its functions are pure and some are not.
1let me = 'Martin';2let us = ['Petra', 'Martin'];34let newMe = me.toUpperCase();5let newUs = us.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);67me === newMe; // false, toUpperCase is pure, it did not mutate the me variable8us === newUs; // true, sort is impure, it mutated the us variable
In @7urtle/lambda functions are made predictable and pure.
1import {upperCaseOf, sort} from '@7urtle/lambda';23let me = 'Martin';4let us = ['Petra', 'Martin'];56let newMe = upperCaseOf(me);7let newUs = sort((a, b) => a < b ? -1 : a > b ? 1 : 0)(us);89me === newMe; // false, upperCaseOf is pure, it did not mutate the me variable10us === newUs; // false, sort is now also pure, it did not mutate the us variable
Because functional programming works with pure functions, all variables are immutable constants. In practice, in JavaScript, that means in your code you should not need to change any variables. All code logic is expressed by functions mapping inputs to outputs.
First-class and higher-order functions
JavaScript has first-class functions because functions are treated like any other value. A function can be passed as an argument to other functions, can be returned by another function and can be assigned as a value to a variable.
Higher-order function is a function that takes a function as its input or it returns a function as its output. It can be obvious to you, because in JavaScript you regularly assign your functions into variables.
1const myFunction = function (value) {2return value + 1;3};4const myArrowFunction = value => value + 1;5// both myFunction and myArrowFunction are pure functions saved in a constant variable67const myFirstClassFunction = function (fn, value) {8return fn(value);9};10const myFirstClassArrowFunction = (fn, value) => fn(value);11// both myFirstClassFunction and myFirstClassArrowFunction expect their first argument fn12// to be a function1314const myCurriedFunction = function (fn) {15return function (value) {16return fn(value);17};18};19const myCurriedArrowFunction = fn => value => fn(value);20// both myCurriedFunction and myCurriedArrowFunction are curried functions that return21// a function after receiving their first argument.2223myCurriedFunction(myFunction)(1) === myCurriedArrowFunction(myFunction)(1); // true
The use of higher-order functions is instrumental to currying, partial application, and function composition.
You have been using higher-order functions all along when you were assigning functions as arguments for click button events:
1document.querySelector('#myButton').addEventListner('click', function() {2alert('click happened inside higher-order function');3});
Partial Application
When defining functions that depend on multiple arguments, you would usually do something like this:
1const add = function (a, b) {2return a + b;3};45add(1, 2); // => 3
In functional programming, the best practice is to define all your functions curried. That means that after your function is called with its first argument it returns a function expecting the next argument until all arguments are received. This approach helps with reusability of your code as your function will lend themselves nicely to be combined into other functions through partial application and function composition.
1const add = a => b => a + b;23add(1)(2); // => 345const add5 = b => add(5)(b);6const add5PointFree = add(5);78add5(5) + add5PointFree(5); // 20
In @7urtle/lambda functions support being called as both curried unary or common n-ary functions so that you can adopt the style that you choose.
1import {substr} from '@7urtle/lambda';23'7urtle'.substr(0, 1); // => '7', original JavaScript4substr(1)(0)('7urtle'); // => '7'5substr(1, 0, '7urtle'); // => '7'
You may also notice that @7urtle/lambda follows a specific logic for order of function inputs to make all functions ready for function composition.
Function composition
Classes in object-oriented programming allow you to organize your code into logical models of the world. Instances of classes have their own scope, you can inherit methods and so on. Functional programming doesn't need classes, instead we model our code by flexible function composition.
Pure functions just map inputs into outputs and we then take those outputs and use them as inputs for our next function in the row. This way simple functions come together to build complex logic.
As an example we will build a functional magic that will take a list of names as its input and it will return an upper-case string of those names alphabetically ordered. Let's do it first imperatively without @7urtle/lambda:
1const names = ['Petra', 'luci', 'MARTIN'];23const sortingAZ = function (ao, bo) {4const a = ao.toUpperCase();5const b = bo.toUpperCase();67if (a < b) {8return -1;9}1011if (a > b) {12return 1;13} else {14return 0;15}16};1718const magic = function (names) {19const sorted = names.sort(sortingAZ);2021let result = '';22sorted.forEach(function (name) {23result = result + name + ' ';24});2526return result.trim().toUpperCase();27};2829magic(names); // LUCI MARTIN PETRA
Now let's do the same declaratively using function composition and @7urtle/lambda:
1import {reduce, sortAlphabetically, upperCaseOf, compose} from '@7urtle/lambda';23const names = ['Petra', 'luci', 'MARTIN'];45const justString = list => reduce('')((a, c) => a + ' ' + c)(list);67const magic = compose(upperCaseOf, justString, sortAlphabetically);8// the same as const magic = value => upperCaseOf(justString(sortAlphabetically(value)));910magic(names); // LUCI MARTIN PETRA
You can easily see how @7urtle/lambda and functional programming made that task a child's play by defining just two functions on two lines of code. Once you get familiar with functional programming syntax, you will be able to write much shorter, reusable, and robust code that follows general programming best practices. Look at more functional programming advantages.
Function composition based on pure functions can also enjoy the advantages of referential transparency. In functional programming, referential transparency is defined as the fact that a pure function (including composed pure functions) may be replaced by its value without changing the result of the program. That has an actual practical impact because you can use memoization and significantly improve the performance of you code by replacing the results of functions through cached values. @7urtle/lambda for that purpose offers a very practical function memo.
Argumentless tacit point-free programming
In functional programming it is typical to use of argumentless tacit (point-free style) of code. It was also used in the previous code example with the function compose.Tacit code allows you to omit explicit declaration of arguments. An example can be see here:
1import {map} from '@7urtle/lambda';23const names = ['Petra', 'luci', 'MARTIN'];45// regular declaration with argument list present6const upperCaseOfArray = list => map(upperCaseOf)(list);78// tacit point-free declaration valid in JavaScript9const upperCaseOfArrayTacit = map(upperCaseOf);1011upperCaseOfArrayTacit(names); // ['PETRA', 'LUCI', 'MARTIN'];
All your curried functions (and all @7urtle/lambda functions) can be declared this way. It makes your code much more concise when you get used to it and you can see many example of tacit declarations elsewhere in Learn articles. It is useful especially with partial application. The example of upperCaseOfArrayTacit
is an example of partial application of the function map.
Unfortunately there are some disadvantages. Practical disadvantage is a low support in IDEs that won't recognize your function as a function and instead it will consider it a variable with a value which leads to wrong coloring (but no errors). You can see the same issue in the code example above. On the other hand, JavaScript has first-class functions, so in the end the function is a value and you might say that the color is actually correct.
Additional disadvantage is that your colleagues may have hard time understanding the code if they are not familiar with tacit programming or the functions that you use. Because of that it should be recommended that you are careful in your use of point-free style. Also you may consider using simple Hindley-Milner inspired comments that would show the function structure to anyone reading the code. The same principle is also used in the @7urtle/lambda source code.
1import {includes, filter} from '@7urtle/lambda';23const animals = ['Hermann\'s Tortoise', 'Dog', 'Russian Tortoise', 'Cat', 'Greek Tortoise'];45// isTortoise :: string -> boolean6const isTortoise = includes('Tortoise');78/**9* @HindleyMilner tortoisesFromArray :: [string] -> [string]10*/11const tortoisesFromArray = filter(isTortoise);1213tortoisesFromArray(animals);14// => ['Hermann's Tortoise', 'Russian Tortoise', 'Greek Tortoise']
Ask For Help
If you have any questions or need any help, please leave a message on @7urtle/lambda GitHub Issues to get quick assistance. If you find any errors in @7urtle/lambda or on the website please reach out.