01 Functional vs Object Oriented Response to Unknown and Uncertain

01 Functional vs Object-Oriented Response to Unknown and Uncertain #

Hello, I’m Ishikawa.

The programming paradigm can be seen as the metacognition of programming languages. Looking at JavaScript from the perspective of programming paradigms, it is a structured, event-driven, dynamic language that supports both declarative and imperative modes. So we can say that JavaScript is a multi-paradigm language, a “rich” language.

image

Among the programming paradigms supported by JavaScript, the most commonly used are Object-oriented Programming (OOP) and Functional Programming (FP), with object-oriented programming being the most popular. There are already many books on object-oriented programming available, but functional programming is relatively less mentioned due to its smaller audience and support from fewer languages.

I guess you may already have some understanding of these two programming paradigms, and you might even be familiar with them. However, I still want to emphasize this topic in the first lesson because when you are learning JavaScript, you may encounter at least one core pain point:

  • If you have already learned traditional object-oriented languages, you may not have a deep understanding and application of functional programming when learning JavaScript.
  • Conversely, if you initially learned JavaScript and only stayed at developing simple applications, you may not have a deep understanding and application of its object-oriented features either.

These two learning difficulties often lead us to learn a concept and then encounter various side effects that we need to learn how to solve, which often leads to giving up easily.

In addition, in the introductory words, I mentioned that functional + reactive programming can counter uncertainty. This concept is not only applied in programming but also a cross-disciplinary research. For example, disciplines in hard sciences such as AI, mechanical engineering, and aerospace engineering, as well as many well-known universities (such as Berkeley, MIT) and government agencies (such as NASA), have conducted in-depth research on System Dynamics and Controls. The core of this research is studying how to achieve system control in dynamic situations, and dealing with fluctuations and disturbances is a crucial aspect of it.

In functional programming, we usually refer to various disruptions as side effects.

So next, I will first take you to the top floor of the “House of Thinking” to understand the core ideas of the JavaScript language, and then show you how to use these two programming paradigms to solve problems in an adapted way. This way, when you encounter known and unknown problems in the future and engage in complex system development, you will be able to find an effective approach.

image

Functional Programming #

First, let’s take a look at functional programming to understand what a function is, what it is used for, what side effects may occur in programming, and how to solve these side effects using JavaScript’s core design principles and tools.

What is a Function and How to Use it? #

A function consists of input, function, and output. It is similar to the functions we learned in middle school. A function is a relationship between a set of data and a goal. Its purpose is to encapsulate behavior in order to achieve the goal.

Image

Let’s take a simple example: we want to implement a tool to calculate consumption tax, with the goal of calculating the tax based on the product price.

In the following code, productPrice is the input parameter, and the actual product price 100 yuan is the passed argument. The function calculateGST itself encapsulates the algorithm, and the output 5 is the returned value, which is the consumption tax of 5 yuan.

function calculateGST(productPrice) {
    return productPrice * 0.05;
}
calculateGST(100); // returns 5

In fact, jQuery, which is commonly used by many developers, is a toolkit. If we open the source code of jQuery on GitHub, we can see that it contains various helper tools. For example, the isArrayLike function below is used to determine whether an object is array-like. This functionality can also exist independently of the jQuery library. This is the basic use of functional programming.

function isArrayLike(obj) {
  var length = !!obj && obj.length,
    type = toType(obj);
	
  if (typeof obj === "function" || isWindow(obj)) {
    return false;
  }
	
  return type === "array" || length === 0 ||
    typeof length === "number" && length > 0 && (length - 1) in obj;
}

As we can see from the acceptable parameters of isArrayLike, the input value of a function can not only be a primitive type, such as a number or a string as shown in the previous example, but also a relatively complex object type, including objects themselves and arrays. Even the function itself, as an object, can be an input or output value. We call this kind of function a higher-order function.

Image

What Side Effects Can Occur in a Function? #

As mentioned earlier, the function encapsulates algorithms, so the function itself is relatively controllable. The less controllable aspect is the external environment. Here, we can divide the uncontrollable external environment into three main categories.

The first category is the most common side effect in functions, which is global variables. In the example below, we first define a global variable x. Then, before logging its value, we execute different functions. However, we cannot guarantee that these functions do not change the value of this variable, and we cannot ensure that the result outputted each time is 1. Therefore, this uncertainty exists from the input onwards.

var x = 1;
foo();
console.log(x);
bar();
console.log(x);
baz();
console.log(x);

Another obvious problem besides global variables is I/O effects. Here, I/O does not refer to the parameters and return values in the previous functions, but rather actions such as user input through a mouse or keyboard in the front-end browser. In the case of Node on the server-side, it refers to the file system, network connections, and the stdin (standard input) and stdout (standard output) streams.

The third most common side effect is related to HTTP requests. For example, if we want to send a network request for a user’s order action, we need to obtain the user ID first and then send it along with the user’s ID. If we initiate the order request before obtaining the user ID, we may receive an error.

Reducing Side Effects: Pure Functions and Immutability #

So how do we reduce these side effects? In functional programming, there are two core concepts: pure functions and immutability.

This is a “two-level loop,” where pure functions primarily address the “inner loop,” while immutability mostly considers the “outer loop.”

Image The term “pure function” refers to a function whose return value depends only on its parameters and does not have any side effects during execution. It means that in the face of external complexity and variability, the encased part of the function must be stable. For example, in the previous example of a sales tax calculator, when the input parameter of product price is 100, the output result will always be 5. Regardless of any interference, it will not return a number other than 5, unless you change the parameter.

Let’s take a look at another example. When the tax rate is extracted from the function and placed outside the function as a variable, it is no longer a pure function, because the calculation result will vary with the change of this variable. Therefore, pure functions can reduce dependencies on uncertain external factors and minimize side effects.

var rate = 0.05;
function calculateGST( productPrice ) {
  return productPrice * rate;
}
calculateGST(100); // returns 5

Besides pure functions, another core concept in functional programming for solving side effects is “immutability”. How to understand this? We can use the built-in splice and slice methods in JavaScript as examples.

const beforeList = [1,2,3,4]
console.log(beforeList.splice(0,2))
console.log(beforeList.splice(0,2))
//[ 1, 2 ]
//[ 3, 4 ]

const beforeList = [1,2,3,4]
console.log(beforeList.slice(0,2))
console.log(beforeList.slice(0,2))
//[ 1, 2 ]
//[ 1, 2 ]

As you can see, the splice method of an array changes the value of the beforeList variable in the global scope after processing the data, so it is mutable. However, the slice method, when executed, does not affect the value of beforeList in the global scope, so it is immutable. This is also why, in development, if we want to ensure immutability, we cannot use splice and should use slice instead.

Therefore, immutability reduces the impact on the program from external sources, as well as reducing the impact on the external world. Because if you pass an external variable as an input parameter, make changes to it within the function, and return the changed value as output, you may not know what kind of result this change will have on the entire system.

Furthermore, in arrays, you can see more examples like splice and slice, which demonstrate pure functions, impure functions, and mutability versus immutability.

Image

In addition, from the design principles of pure functions and immutability, we can abstract a concept.

Because “side effects” are first and foremost an action (effect), and actions follow a cause and effect relationship. Therefore, from the perspective of values, “pure functions” affect values only once, while “immutability” does not affect values at all.

How should we understand that “pure functions” only affect values once? This is where the concept of idempotence comes in. If you have worked on a large transactional application before, you should be familiar with this concept. For example, sometimes users may repeatedly send update requests for the same order, and the results should be consistent.

In mathematics, idempotence means that no matter how many times we nest and execute a function, the result should be the same. For example, in this Math.round rounding example, no matter how many times you nest and execute it, the result will be the same.

// Mathematical idempotence
Math.round(((0.5)))

In computer science, idempotence means that running a program multiple times will yield the same result. For example, let’s say we have an adder function that always returns 7 when adding 3 and 4. So, you can actually apply the mathematical concept here.

// Computer idempotence
adder(3, 4) // returns 7
adder(3, 4) // returns 7

Okay, then let’s look at how to understand that “immutability” does not affect values at all.

From the examples of slice and splice in the previous arrays, you should be able to feel that splice is more like a piece of playdough, initially it might be a block, and you can shape it into legs and head, resulting in a small person, meaning it has changed. On the other hand, slice forms a new array after processing, while the original array remains intact. It treats values as Lego blocks rather than playdough. When you apply this mindset to state management, you can track changes in states without tampering with them.

Image

In summary, the most important aspect of functional programming is the input-output relationship and the algorithm in between. The core problem we need to solve is side effects. To address side effects, we need to understand two important concepts: pure functions and immutability. “Pure functions” emphasize their own stability and only affect the result once, while “immutability” emphasizes minimizing negative effects in interaction with the external world.

Image

Object-oriented Programming #

Let’s take a look at object-oriented programming (OOP). As mentioned before, if we use functions to create a tax calculation tool or determine if a number is an array-like object, there is no problem, and we can be assured that if we want it to be “pure”, its results can be based on the rules we define, without surprises or unexpected behavior. So, is that enough? Why do we need objects? Let’s explore further.

What are objects and how are they created? #

In the introduction, I mentioned an example of “crossing the river by feeling the stones”. First, there has to be “you” standing on the riverbank, and this “you” is an object. Even if you have a tool (function) like a speedboat on the riverbank, it can only dock there. Or you have the method of swimming, but it only works when applied to you.

This is the essence of objects. When developing business systems, we encounter various business objects, such as “form”, “shopping cart”, and “order”. These can all be viewed as objects. Therefore, we say that tools and methods usually serve objects.

As an example, let’s say we have a widget object. We want to define a property called widgetName for it, and give it a function called identify to identify its own name. In JavaScript, we can achieve this with the following code:

var widget = {
  widgetName: "Widget",
  identify: function() {
    return "This is a " + this.widgetName;
  }
};

console.log(widget.widgetName); // Returns "Widget"
console.log(widget.identify()); // Returns "This is a Widget"

Why do we need encapsulation, reusability, and inheritance? #

In reality, if functions and objects form productivity, then encapsulation, reusability, and inheritance form production relationships.

Image

Encapsulation is commonly used when designing components. For example, on a travel website page, the filters, calendars, and result areas can all be considered as different modules or components. These components are imported and loaded through encapsulation.

Reusability involves abstracting reusable functionality into a class, where only an instance of the class is created each time. For example, if there are many buttons on our page with similar functionality, we can abstract them into a class, and each button becomes an instance of the button class.

Of course, the above buttons may have specific differences. In this case, we can put the common functionality in an abstract class, and place specific behaviors or properties in an implementation class through inheritance. This way, we can make changes in the child class based on the basic functionality inherited from the parent class.

However, if there are too many layers of hierarchy between parent and child classes, it can become bureaucratic. If the parent class has issues, it affects the entire system, and an excessive level of abstraction can make the code difficult to understand.

In reality, composition also exists in object-oriented programming. It means that a subclass is not inheriting from a specific parent class, but rather forming a class through the composition of multiple classes. This is similar to the modern workplace, where companies have agile teams internally to deal with external competition. By combining the individual value and team “composition”, each team member contributes more value than the sum of their individual contributions, rather than relying on a subordinate relationship.

Therefore, in object-oriented programming, there is also the concept of “composition over inheritance”. However, in practice, inheritance is not completely discouraged. The choice of which approach to use should depend on the specific circumstances.

What is prototype-based inheritance? #

Now that we have talked about inheritance, we need to clarify what prototype-based inheritance means.

First, let’s understand one thing: what are the differences between classes and objects in JavaScript compared to other object-oriented programming languages?

In traditional object-oriented programming languages like Java, an object is created based on a “blueprint” called a class. However, in JavaScript, there is no such copy-dependent relationship between classes and objects. In fact, objects and “classes” in JS are connected through prototype chains.

For example, in the class-based example on the left side of the following diagram, two instances can be created based on a class blueprint. In the prototype-based example on the right side, we can see that the two objects created by a constructor function are connected through the prototype chain and the prototype of the constructor function. They are not copies of a blueprint and do not have a dependent relationship.

Image

Although JavaScript has also introduced classes after ES6, it is still based on prototype-based object-oriented programming at its core, even though it uses class syntax as syntactic sugar.

In ES6+, the usage of class syntax is similar to before, with the main difference being the use of the class keyword:

class Widget {
  constructor(){
    // specify here
  }
  notice(){
    // method implementation
  }
}

Markdown转换结果 #

Object-oriented Programming #

Let’s take a look at object-oriented programming (OOP). As mentioned before, if we use functions to create a tax calculation tool or determine if a number is an array-like object, there is no problem, and we can be assured that if we want it to be “pure”, its results can be based on the rules we define, without surprises or unexpected behavior. So, is that enough? Why do we need objects? Let’s explore further.

What are objects and how are they created? #

In the introduction, I mentioned an example of “crossing the river by feeling the stones”. First, there has to be “you” standing on the riverbank, and this “you” is an object. Even if you have a tool (function) like a speedboat on the riverbank, it can only dock there. Or you have the method of swimming, but it only works when applied to you.

This is the essence of objects. When developing business systems, we encounter various business objects, such as “form”, “shopping cart”, and “order”. These can all be viewed as objects. Therefore, we say that tools and methods usually serve objects.

As an example, let’s say we have a widget object. We want to define a property called widgetName for it, and give it a function called identify to identify its own name. In JavaScript, we can achieve this with the following code:

var widget = {
  widgetName: "Widget",
  identify: function() {
    return "This is a " + this.widgetName;
  }
};

console.log(widget.widgetName); // Returns "Widget"
console.log(widget.identify()); // Returns "This is a Widget"

Why do we need encapsulation, reusability, and inheritance? #

In reality, if functions and objects form productivity, then encapsulation, reusability, and inheritance form production relationships.

Image

Encapsulation is commonly used when designing components. For example, on a travel website page, the filters, calendars, and result areas can all be considered as different modules or components. These components are imported and loaded through encapsulation.

Reusability involves abstracting reusable functionality into a class, where only an instance of the class is created each time. For example, if there are many buttons on our page with similar functionality, we can abstract them into a class, and each button becomes an instance of the button class.

Of course, the above buttons may have specific differences. In this case, we can put the common functionality in an abstract class, and place specific behaviors or properties in an implementation class through inheritance. This way, we can make changes in the child class based on the basic functionality inherited from the parent class.

However, if there are too many layers of hierarchy between parent and child classes, it can become bureaucratic. If the parent class has issues, it affects the entire system, and an excessive level of abstraction can make the code difficult to understand.

In reality, composition also exists in object-oriented programming. It means that a subclass is not inheriting from a specific parent class, but rather forming a class through the composition of multiple classes. This is similar to the modern workplace, where companies have agile teams internally to deal with external competition. By combining the individual value and team “composition”, each team member contributes more value than the sum of their individual contributions, rather than relying on a subordinate relationship.

Therefore, in object-oriented programming, there is also the concept of “composition over inheritance”. However, in practice, inheritance is not completely discouraged. The choice of which approach to use should depend on the specific circumstances.

What is prototype-based inheritance? #

Now that we have talked about inheritance, we need to clarify what prototype-based inheritance means.

First, let’s understand one thing: what are the differences between classes and objects in JavaScript compared to other object-oriented programming languages?

In traditional object-oriented programming languages like Java, an object is created based on a “blueprint” called a class. However, in JavaScript, there is no such copy-dependent relationship between classes and objects. In fact, objects and “classes” in JS are connected through prototype chains.

For example, in the class-based example on the left side of the following diagram, two instances can be created based on a class blueprint. In the prototype-based example on the right side, we can see that the two objects created by a constructor function are connected through the prototype chain and the prototype of the constructor function. They are not copies of a blueprint and do not have a dependent relationship.

Image

Although JavaScript has also introduced classes after ES6, it is still based on prototype-based object-oriented programming at its core, even though it uses class syntax as syntactic sugar.

In ES6+, the usage of class syntax is similar to before, with the main difference being the use of the class keyword:

class Widget {
  constructor(){
    // specify here
  }
  notice(){
    // method implementation
  }
}
console.log("notice me");

}

display() { console.log(“display me”); } }

var widget1 = new Widget();

widget1.notice(); widget1.display();

Okay, let’s observe the prototype chain with another example. In the code below, we use the call() method provided by the function and the Object.create() method provided by the object to let Notice inherit the properties and methods of Widget as a subclass. Then we create two instances, notice1 and notice2.

At this point, if we use getPrototypeOf to get the prototype of notice1 and notice2, we will find that they are equal to the prototype of Notice. When we call the display method, we actually call the method in the prototype of Notice in the prototype chain.

function Widget(widgetName) {
    this.widgetName = widgetName;
}

Widget.prototype.identify = function () {
    return "This is " + this.widgetName;
};

function Notice(widgetName) {
    Widget.call(this, widgetName);
}

Notice.prototype = Object.create(Widget.prototype);

Notice.prototype.display = function () {
    console.log("Hello, " + this.identify() + ".");
};

var notice1 = new Notice("App A");
var notice2 = new Notice("App B");

Object.getPrototypeOf(notice1) === Notice.prototype; // true
Object.getPrototypeOf(notice2) === Notice.prototype; // true

notice1.display(); // "Hello, This is App A"
notice2.display(); // "Hello, This is App B"

This confirms what was mentioned earlier, in traditional object-oriented languages like Java, when we use inheritance, the properties and functionalities of a class can be “copied” to the objects created based on that class. However, in JavaScript, even though we create notice1 and notice2 using Notice, they do not have their properties and functionalities copied over. Instead, they rely on the prototype chain to find the functionalities in the prototype and use “linking” instead of “copying”.

for (var method in Notice.prototype) {
    console.log("found: " + method);
}
// found: display
// found: identify

Therefore, through the above for-in loop, we can find all the functionalities in the prototype chain. If we want to see which functionalities the prototype object of the Notice function has, we can see that it returns display and identify. This proves that, in addition to its own prototype object and its own display functionality, it also links to the identify functionality in Widget.

Image

Now we know that the most core aspect of object-oriented programming is serving business objects, and the most significant problems to solve are encapsulation, reuse, and inheritance. In JavaScript, the special nature of object-oriented programming is based on prototype chain inheritance, which is more like “authorization” rather than traditional “parent-child” inheritance. To address the problem of excessive inheritance levels, the philosophy of composition over inheritance also exists in object-oriented programming.

Image

Summary #

In this class, we mainly learned about the core concepts of functional programming and object-oriented programming. One is about managing and solving side effects, while the other serves business objects.

Understanding this part is of great significance for us to further understand these two programming paradigms, as well as various data structures and algorithms, various design patterns in JavaScript, and so on. It provides a solid theoretical foundation for us to learn and apply JavaScript more effectively.

Thought Question #

When we talk about functional programming, we mention the concepts of immutability and pure functions to solve side effects. In this regard, do you think constants (const, constant) in JavaScript can be considered as immutable?

Feel free to share your answer, exchange learning experiences, or ask questions in the comments section. If you find it valuable, you are also welcome to share today’s content with more friends.

Further Reading #