Purely Functional Programming in JavaScript
Sergey Shandar
Posted on September 9, 2023
There are a lot of articles, videos, and blog posts about functional programming using different programming languages, including JavaScript.
Usually, the main topic of these articles is how to use various functional programming paradigms, such as first-class functions, immutable objects, and currying.
Nevertheless, the primary value of purely functional programming languages is an absence of side effects. Partial applications of different functional paradigms in impure languages, such as JavaScript, may reduce the number of side effects but don't guarantee their complete elimination.
Side effects reduce scalability and the ability to replace components and platforms. So, it is preferable to reduce the number of side effects to a bare minimum.
There are dozens of purely functional programming languages. Some of them are pretty successful in the software development industry - for example, Haskell, Elm, and PureScript. However, the most popular programming language is JavaScript, and it is not purely functional.
The main reason to use JavaScript, besides its popularity, is that almost any web browser can run it. Also, one of the most popular data interchange and file formats is JSON, a subset of JavaScript. Because of this JSON/JavaScript relation, serialization in JavaScript is more straightforward than in other programming languages. In my experience, object-oriented programming languages usually have the biggest challenges in serialization.
Any working program has side effects such as input/output, functions that return the current time, or random numbers.
But it is possible to write a big part of a program without using impure functions. An impure function can be rewritten as a pure function.
For example:
const addAndPrint = a => b => {
const result = a + b
console.log(result)
return result
}
const pureAddAndPrint = log => a => b => {
const result = a + b
log(result)
return result
}
Pure functions are much more flexible. A developer may use the pureAddAndPrint
function with either pure or impure arguments, such as console.log
. Some platforms may not have console.log
, and in that case, a developer could provide a replacement for it.
Another use case is unit testing, and a developer may create a mock function and pass it as an argument.
Currying
You may notice function declarations in this article use currying. In most purely functional programming languages, a function can accept only one argument, and currying is a way to provide multiple arguments to a function.
Another way is to use a tuple as an argument:
const tupleAddAndPrint = ([log, a, b]) => {
const result = a + b
log(result)
return result
}
However, currying can simplify partial function applications:
// using currying
const consoleLogAddCurry = pureAddAndPrint(console.log)
// using tuples
const consoleLogAddTuple = ([a, b]) =>
tupleAddAndPrint([console.log, a, b])
Safety
Usually, purely functional languages provide better safety. A pure function can’t access data outside passed arguments. On the contrary, an impure function can access almost anything, which increases the probability of vulnerabilities.
One such example is the famous Log4Shell. Log4j is written in an impure language (Java), and users were not aware it uses HTTPS to download and run code. A pure implementation of Log4j would require an HTTPS protocol as an argument.
In this case, users have some level of control, and, most likely, they would provide a stub instead of an actual HTTPS protocol. Pure functions do not provide absolute protection, but they can significantly reduce the probability of vulnerabilities.
FunctionalScript
It is possible to write purely functional code in an impure language. FunctionalScript is an attempt to create a purely functional subset of JavaScript, and the subset should not have the ability to create a function with side effects.
Because FunctionalScript is a subset of JavaScript, we do not need to develop compilers, transpilers, debuggers, IDEs, and other development tools for the language.
Also, developers do not need to learn an entirely new programming language and how it interacts with other systems and languages. FunctionalScript is an open specification and has no risk of vendor lock-in.
Even if the FunctionalScript specification disappears completely, any FunctionalScript code will still work like any other JavaScript code.
Recursion Problem
Most purely functional programming languages have no loops because all data is immutable.
Instead, developers use recursion:
const factorial = n => n <= 0 ? 1 : n * factorial(n - 1)
Recursion consumes stack, and it can cause a stack overflow in case of too many recursive calls. Functional languages solve this problem by tail call elimination. Note that a compiler can only eliminate a call if it’s the last call or operation.
For example, a tail call elimination can not be applied to our factorial function because the last operation is multiplication instead of factorial. However, we can change the function so that the tail call elimination can be applied.
const factorialTail = result => n =>
n <= 0 ? result : factorialTail(result * n)(n - 1)
const factorial = factorialTail(1)
The JavaScript standard (ECMAScript 6) supports the tail call elimination (aka a proper tail call), but V8 and SpiderMonkey do not. That means that Google Chrome, Microsoft Edge, Node.js, and Firefox do not support PTC. So, de facto, JavaScript has no PTC.
Loops in FunctionalScript
The problem is that FunctionalScript objects are immutable, and, as shown above, we can’t use recursion for iterations.
FunctionalScript allows reassigning of local variables declared with let as a workaround for this problem, and such variables can only be used inside a function where the variables are declared.
const factorial = n => {
let i = n
let result = 1
while (i > 1) {
result = result * i
i = i - 1
}
return result
}
WebAssembly
WebAssembly allows developers to create web applications using almost any programming language. It is derived from asm.js, which is also a subset of JavaScript.
Advantages:
- near-native code execution speed,
- different programming languages support compilation to WebAssembly.
Disadvantages:
- requires additional build steps and tools,
- WebAssembly programs should interact with DOM and other JavaScript API using a language interoperability layer.
asm.js inspired FunctionalScript as a subset of JavaScript. Compared to asm.js and WebAssembly, FunctionalScript is a high-level programming language. Theoretically, it is possible to create JIT and AOT compilers from FunctionalScript to WebAssembly, or any other assembly language.
Compared to JavaScript, a compiler from FunctionalScript may generate more optimal code because similar FunctionalScript code is more deterministic.
For example, FunctionalScript can use a reference counter instead of a proper garbage collector because immutable data can not have circular references.
Also, other purely functional programming languages, such as Elm, can use FunctionalScript as a compilation target.
FunctionalScript API Limitations
As was mentioned earlier, FunctionalScript can not directly call functions with side effects. Because JavaScript API has many impure functions, only a limited subset of JavaScript API is available to FunctionalScript.
However, a JavaScript program can pass impure functions to FunctionalScript modules.
Typing
FunctionalScript derived a dynamic type system from JavaScript. Nevertheless, it is possible to use JSDoc type annotations and a TypeScript compiler as a validator. For example
/** @type {(a: number) => (b: number) => number} */
const add = a => b => a + b
See TypeScript JSDoc Reference for more details.
TypeScript uses a structural type system instead of a nominal type system. Languages with a nominal type system may cause typecasting problems in big projects with many third-party modules. For example, two definitions of Vector3D are not compatible, and adapters are required. Because of this, structural type systems enhance modularization and code reuse.
Modules and Packages
FunctionalScript uses a Node.js package manager (npm) and CommonJS as a module system. CommonJS is easy to implement even without a FunctionalScript parser.
Because FunctionalScript is a purely functional language, a FunctionalScript module can only reference another FunctionalScript module. But, a JavaScript module can reference any FunctionalScript module.
Currently, FunctionalScript does not support ECMAScript Modules and asynchronous modules.
JSON Modules
CommonJS supports loading JSON files as JavaScript modules. Because JSON contains only data, any JSON file is also a FunctionalScript module.
Note that the loading procedure differs for JSON and JavaScript files, even if JSON is a subset of JavaScript.
A JSON module declares all public exports in the first expression.
{
"a": "Hello",
"b": 42
}
A JavaScript Common.js module declares all public exports in module.exports
.
module.exports = {
a: "Hello",
b: 42
}
Applications
FunctionalScript code can be used in any JavaScript/TypeScript application. Because FunctionalScript code has no direct access to IO, the same code can be used on different platforms, for example, web-browser, Node.js.
FunctionalScript is a superset of JSON. Because it has no side effects, it can be used as a JSON with pure functions and expressions, for example, in configuration files.
Another application is a query language as an alternative to SQL and LINQ.
Posted on September 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.