banner
二階堂春希

春希のブログ

山雨欲来风满楼,故攻八面以铸无双。 孤战非所望,俗安不可期。
tg_channel
telegram
twitter
github

The application of functional reactive programming in web development

Recently, I have taken on several large web development projects, and combined with my previous experience with solid.js, I have gradually become proficient in functional reactive programming. Functional reactive programming is quite useful in large web projects.

The more you practice, the more you will gain; I have experienced this firsthand. I didn't expect to learn concepts from the top of the hierarchy of functional programming at the bottom of the hierarchy of web development.

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 there are not many people using 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 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, typically 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. Moreover, 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 very 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 effortlessly 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, which support functional programming and are commonly used for web development, and Haskell, which is a functional programming language that supports 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's types are all generic).

Specifically, the simplicity of JS lies in:

  1. 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.
  2. 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 is no need to worry about the lifecycle of closure variables or whether a closure can be called repeatedly.
  3. Lambda expressions can be written casually. Even someone who knows nothing about functional programming can easily use methods like .map() and insert a Lambda expression into them.

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 (like 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:

  1. Pure Functions: This is the core of functional programming. A pure function is one whose output depends only on its 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.
  2. 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 because there is no need to worry about data being accidentally changed in different parts of the program.
  3. 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.
  4. 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.
  5. Closures: These are functions that can capture variables from their outer scope. Closures allow functions to use those variables even outside their defining environment.
  6. Recursion: Since functional programming typically does not use loop constructs (like for or while loops), recursion becomes the primary method for executing repetitive tasks.
  7. 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 (like 3) without affecting other parts of the program.
  8. Lazy Evaluation: In functional programming, expressions are not computed immediately when bound to variables but are computed only when their results are needed. This can improve efficiency, especially for large datasets.
  9. Pattern Matching: This is a technique for checking data and selecting different operations based on its structure. In some functional languages, pattern matching is used to simplify operations on complex data structures.
  10. Function Composition: In functional programming, functions can be combined to build more complex operations, where the output of one function directly becomes the input of another.
  11. 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 will inevitably be at a loss about how to test due to side effects. Developing the habit of writing pure functions is important.

Monad#

This concept deserves to be discussed separately because it is somewhat difficult to understand 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 its unit 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 is 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, which represents an asynchronous operation that will complete in the future but has not yet completed.

Through the .then() method (bind), we can define the operations to perform on the data when the Promise is successful.

In the example above, we encapsulated the function containing side effects (readFileSync) with a monad, and no side effect operations were executed until the bind was completed (i.e., at the semicolon of jsonResult.appName). Only at the end (i.e., title = await titlePromise;) were the side effect operations 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 consists of 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 is 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 the type being a Monad data structure.
  • bind is a pure function that computes values (it does not need to know about Monad).

Here is 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 surely familiar with this concept.

Simply put, when a or b changes, a+b will automatically change.

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. This is typical of reactive programming.

So, it is not difficult to guess: functional reactive programming = functional programming + reactive programming.

Functional reactive programming handles data streams using Monads (observable sequences). There are frameworks in the front end that utilize the 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 it.

Practice makes perfect.

Taking the First Step#

There are three very important functions in functional programming:

  1. Map:

    • Function: The map function is mainly used to apply the same function to each element in a collection and return a new collection containing the elements after applying the function.
    • Example: For instance, if you have a list of numbers [1, 2, 3], using the map function can multiply each number by 2, resulting in [2, 4, 6].
  2. Reduce:

    • Function: The reduce function is typically used to combine all elements in a collection into a single result in some way. It continuously applies an operation to each element of the collection and the accumulated result so far.
    • Example: If you have a list of numbers [1, 2, 3, 4], using the reduce function can sum them up to get 10 (1+2+3+4).
  3. 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 the filter function can select all numbers greater than 2, resulting in [3, 4, 5].

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, which I cannot disclose; the other is open-source.

The functional reactive programming project 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 look at it.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.