Recently, I have taken on several large web development projects, and combined with my previous experience writing solid.js, I have gradually become proficient in functional reactive programming. Functional reactive programming is quite useful in large web projects.
With more practice, there will always be gains; I have experienced this. I didn't expect to learn concepts from the top of the hierarchy of FP in the web development field, which is often looked down upon.
Javascript is the Orthodoxy of Functional Programming#
The most typical functional programming language should be Haskell. However, although Haskell does have the servant framework, I think very few people use Haskell to write web services because it is just too painful.
However, there are indeed functional programming languages suitable for web development, such as elixir. This language has a very famous project, the policr-mini Telegram Bot, which is written in this language. In addition, there is a programming language similar to Java but supports functional programming called Scala. According to the GitHub repository, Twitter's algorithm is written in this language.
Many people do not realize that Javascript, which has a low entry threshold and extremely loose syntax, is generally used by less capable programmers to write front-end code, is actually very suitable for writing large web services using functional programming.
The performance of the V8 engine is actually not low; it just consumes more memory for small projects. Additionally, due to technological advancements, current development often focuses more on development costs rather than performance, so the performance loss of JS is not that important.
JS's Support for Functional Programming#
JS's type system is quite special; it seems as if there are no types at all. This lack of types can feel difficult to maintain, which is why Typescript was invented. However, from another perspective, JS's type system meets the requirements of functional programming:
- First-Class Functions: Functions can be passed as parameters to other functions, returned as values, assigned to variables, or stored in data structures. Clearly, JS can do this.
- Closures: A closure is a function along with its captured lexical environment. JS programmers often write closures easily without thinking, which can lead to unexpected memory leaks.
- Higher-Order Functions: Higher-order functions are functions that take other functions as parameters or return functions as results. JS frequently uses higher-order functions, such as various callbacks and methods on arrays (or general objects), which are all higher-order functions.
Why JS is So Simple#
Compared to languages like Rust that support functional programming and Haskell, which is a functional programming language suitable for web development, JS is much simpler to write. The main reason is that JS relies on garbage collection, has the typeof
keyword, and is untyped (although TS has types, TS types are all generic).
Specifically, the simplicity of JS lies in:
- No need to write types. Even in Typescript, you can use the
typeof
keyword to extract a variable's type for type calculations and declarations. - No need to consider the lifecycle of closure variables. Unlike Rust's zero-cost abstractions or C++'s manual management, JS uses a simpler garbage collection mechanism, so there's no need to worry about the lifecycle of closure variables or whether a closure can be called repeatedly.
- Lambda expressions can be written casually. Even someone who knows nothing about functional programming can easily use methods like
.map()
and throw in a Lambda expression.
Basic Concepts of Functional Programming#
Functional programming uses functions as the primary means of building software systems. It is significantly different from common imperative programming (such as object-oriented programming).
Unlike imperative programming, which specifies how the machine should do things, functional programming focuses on what the machine should do.
To get started with functional programming, it is very important to understand the following basic concepts:
- Pure Functions: This is the core of functional programming. A pure function is one where the output depends only on the input, has no hidden inputs (such as capturing external variables), and has no side effects during execution (such as not modifying global state or controlling IO). Pure functions are very easy to test.
- Immutability: In functional programming, data is immutable. This means that once data is created, it cannot be changed. All data changes are achieved by creating new data structures rather than modifying existing ones. This helps reduce the complexity of programs, as there is no need to worry about data being accidentally changed in different parts of the program.
- First-Class Functions: In functional languages, functions are treated as "first-class citizens," meaning they can be passed and manipulated like any other data type. You can pass functions as parameters to other functions, return functions from functions, or store them in data structures.
- Higher-Order Functions: Functions that take other functions as parameters or return functions as results. This is a powerful tool for composition and abstraction in functional programming.
- Closures: This refers to functions that can capture variables from their outer scope. Closures allow functions to use those variables even outside their defining environment.
- Recursion: Since functional programming typically does not use loop structures (like for or while loops), recursion becomes the primary method for executing repetitive tasks.
- Referential Transparency: An expression can be replaced with its computed result without changing the program, a property known as referential transparency. This means that a function call (like
add(1,2)
) can be replaced with its output (like3
) without affecting other parts of the program. - Lazy Evaluation: In functional programming, expressions are not immediately computed when bound to variables but are computed only when their results are needed. This can improve efficiency, especially for large datasets.
- Pattern Matching: This is a technique for examining data and selecting different operations based on its structure. In some functional languages, pattern matching is used to simplify operations on complex data structures.
- Function Composition: In functional programming, functions can be composed to build more complex operations, meaning the output of one function directly becomes the input of another.
- Monads: This is a common abstract concept in functional programming used to handle side effects, state changes, etc. Monads provide a structure that allows a series of functions to be applied sequentially while avoiding common side effects.
Most front-end developers know to use jest for testing, but if you don't write pure functions, you'll definitely find it frustrating to test due to side effects. Developing the habit of writing pure functions is important.
Monad#
This concept is worth discussing separately because it has a certain level of understanding difficulty and is commonly used.
The concept of Monad comes from category theory and is used by functional programming languages to implement lazy evaluation and delayed side effects. From a mathematical perspective, a Monad is a monoid in the category of endofunctors, with its binary operation defined as the bind
operation, and the unit element implemented as the return
operation (i.e., identity transformation).
However, since programmers are notoriously not well-versed in mathematics, few people can understand such explanations. It's better to look at how this concept is proposed and applied.
In functional programming, we emphasize pure functions and the absence of side effects. However, in practical applications, this is almost impossible. For example, in web development, you may need to perform database queries, generate random numbers, or write to a database, all of which must rely on side effects.
We want to delay the execution of side effects and use lazy evaluation to execute these side effects only when needed. Monads are what enable delayed side effects.
Promise is a Monad#
In JS, there is a natural Monad, which is Promise.
Promise, as a native construct in JavaScript, provides an elegant way to handle asynchronous operations. It can be seen as an instance of a Monad because it satisfies the two basic operations of a Monad: bind
(the .then()
method in Promise) and return
(the Promise.resolve()
in Promise). Using Promise, we can chain asynchronous operations while maintaining code readability and maintainability.
For example, if you read a configuration file:
const reading = new Promise((resolve, reject) => {
const file = fs.readFileSync('./config.json');
resolve(file);
});
titlePromise = reading.then(file => JSON.parse(file)).then(jsonResult => jsonResult.appName);
title = await titlePromise;
Here, the fetch
function returns a Promise object, representing an asynchronous operation that will complete in the future but has not yet completed.
Through the .then()
method (the bind
), we can define the operations to be performed on the data when the Promise is successful.
In the example above, we encapsulate the function with side effects (readFileSync
) using a monad, and no side effect operations are executed until the bind
is completed (at the semicolon after jsonResult.appName
). Only at the end (title = await titlePromise;
) is the side effect operation executed.
Imagine how to debug the above code. For the parts without side effects, we can use pure function debugging methods. For the parts with side effects, we can debug them separately without worrying about whether the subsequent processing logic is incorrect.
Definition of Monad#
With the above example, it becomes easier to understand the definition. Now, let's explain what it means to be a monoid in the category of endofunctors.
Explaining categories can be quite challenging, as it is inherently abstract. Simply put, a category is a collection of points along with arrows between those points. These points are abstract and can even be the category itself.
A mapping from one category to another is called a functor, and if both the starting and ending points are the same, it is called an endofunctor. When the abstract points are endofunctors, the constructed category is called an endo-category.
In mathematics, a monoid is an algebraic structure consisting of a set of elements, a binary operation, and a unit element. This binary operation must be closed (the result of the operation remains within the category), associative ((a+b)+c = a+(b+c)
), and the unit element must be neutral under this operation (0+n=n
).
From Abstraction to Implementation#
Monads are implemented as a data structure that contains two methods, and sometimes an unwrap
method.
Promises in JS and TS do not have an unwrap.
type Monad<A> = Promise<A>;
function pure<A>(a: A): Monad<A> {
// Since return is a keyword, we use pure here
return Promise.resolve(a);
}
function bind<A, B>(monad: Monad<A>, func: (a: A) => Monad<B>): Monad<B> {
return monad.then(func);
}
return
(pure
) requires the returned content to be a parameter, with a type of Monad data structure.bind
is a pure function that computes values (it does not need to know aboutMonad
).
Here’s a Typescript implementation with unwrap
:
export default interface Monad<T> {
unwrap(): T;
bind: <U = T> (func: (value: T) => Monad<U>) => Monad<U>;
}
export function pure<T>(initValue: T): Monad<T> {
const value: T = initValue;
return Object.freeze({
unwrap: () => value,
bind: <U = T>(func: (value: T) => Monad<U>) => func(value)
});
}
export const Monad = pure;
Reactive Programming and Functional Reactive Programming#
Readers who have written with Vue or Svelte are certainly familiar with this concept.
In simple terms, when a
or b
changes, a+b
will change automatically.
When we develop with Vue, as soon as any bound data changes, the related data and visuals will also change, and developers do not need to write code about "how to notify that a change has occurred" (like ref.value = newValue
); they only need to focus on what to do when a change occurs, which is a typical example of reactive programming.
So, it’s not hard to guess: functional reactive programming = functional programming + reactive programming.
Functional reactive programming handles data streams with Monads (observable sequences). There are frameworks in the front end that utilize lazy evaluation of monads to achieve reactivity, storing state in monads. This framework is called Solid.js.
Finding projects written with this framework will naturally help you understand.
Practice more if you're not skilled.
Taking the First Step#
There are three very important functions in functional programming, which are:
-
Map:
- Function: The
map
function is primarily used to apply the same function to each element in a collection and return a new collection containing the elements after the function has been applied. - Example: For instance, if you have a list of numbers
[1, 2, 3]
, using themap
function can multiply each number by 2, resulting in[2, 4, 6]
.
- Function: The
-
Reduce:
- Function: The
reduce
function is usually used to merge all elements in a collection into a single result in some way. It continuously applies an operation to each element in the collection and the accumulated result so far. - Example: If you have a list of numbers
[1, 2, 3, 4]
, using thereduce
function can sum them up to get 10 (1+2+3+4).
- Function: The
-
Filter:
- Function: The
filter
function is used to select elements from a collection that meet specific criteria, forming a new collection. - Example: Suppose you have a list of numbers
[1, 2, 3, 4, 5]
, using thefilter
function can select all numbers greater than 2, resulting in[3, 4, 5]
.
- Function: The
These three functions are side-effect-free, meaning they do not change the original data collection but generate new collections. This is an important feature of functional programming that emphasizes "immutability."
At the time of writing this article, I am responsible for two large full-stack web projects, one of which is a closed-source program written for my organization and cannot be disclosed; the other is open-source.
The project for functional reactive programming is called NodeBoard-Core, aiming to be a higher-performance, more extensible, more maintainable, easier to deploy, and more secure alternative to v2board. If you are interested in this project, please contact [email protected].
Please, come visit my main blog site; if I don't promote it, no one will see it.