Functor is called Functor in English. Before we get to Functor, let’s talk about containers. Containers contain values and their deformation relations. So containers are functions that contain and process values.
A functor is a special container, and we can think of a functor as a box, and the box has a value in it, and the box is going to expose a method, a method we call a map, and the map method is going to take a parameter, and the parameter is a function that handles the value. That’s the basic idea of a functor.
First of all, why do we learn functors? Well, we know that functional programming is based on mathematical ideas, so for example, our pure functions are actually functions in mathematics, so the functors we’re going to learn are also based on mathematics.
It’s based on the category theory of mathematics, we’re not going to go into the category wheel here, it’s a little bit more complicated.
So we didn’t learn how to control side effects in the process of learning functional programming, because side effects can make our functions become impure. Although side effects are bad, we can’t completely avoid them, so we should try to control side effects in a controllable range.
We can use functors to control side effects, and of course we can use functors to control exceptions, to do asynchronous operations, and so on.
A functor is an ordinary object that maintains a value and exposes a map method, so we can describe a functor as a class, because a functor is a Container. The name of the class is Container.
class Container {
map () {
}
}
Copy the code
When we create a functor the functor has to have a value inside it, so in the constructor we’re going to pass in that value called value, and inside the functor we’re going to store that value, and notice that this value is maintained inside the functor, only the functor knows about it, and that value is contained in a box, not public.
We agree that all members starting with an underscore are private members, so we accept this._value here.
class Container {
constructor(value) {
this._value = value;
}
map () {
}
}
Copy the code
We’re going to expose a map method in this box, and the map method is a function that receives the value, so this function is also a pure function, because we’re going to pass this function to this function, and this function is going to process the value.
So our map takes the parameter fn, and in the map method we’re going to process that value and return a new Container box, which is a new functor new Container.
So when we return a new functor, we’re going to pass that value to the Container, so fn(this._value)
class Container {
constructor(value) {
this._value = value;
}
map (fn) {
return new Container(fn(this._value)); }}Copy the code
So this is a basic functor, and inside the functor we’re going to maintain a basic value, which we’re not going to publish, and we’re going to provide a map method, and the map method is going to take a function that handles the value, and it’s going to return a new functor, and the value in the new functor is the value that the handler processed.
So we’re going to create a Container functor, we’re going to pass in a 5, and then we’re going to want to process the values inside of the functor, and we’re going to call map, and when we call map we’re going to pass in a function, and that function is going to take a parameter, because it’s going to process the values inside of the Container, Suppose we want to increase the value inside the functor by 1.
The map returns a new functor, the new functor we can still call its map method, we can continue to process the value in the new functor, we started with a value of 5, we got a value of 6 after the map, we can continue to map that value.
const r = new Container(5).map(x= > x + 1);
console.log(r);
Copy the code
So r is a Container object, and the _value in that object is 6. Our map method ultimately returns not a value, but a new functor object, and we store the new value in the new functor object. We never publish the value. If we want to process the value, we pass a function to the Map object that processes the value.
So every time we want to create a functor, we have to call a new, which is a little bit inconvenient, so we can encapsulate the new Container.
In order to distinguish ourselves from object-oriented methods, instead of using new to create functors, we can create a static method called “of” in a Container, which returns a functor object. When we create a functor object, we need to pass a value, so the “of” method takes a value and passes it to the object.
In fact, the of method encapsulates the new keyword, just to distinguish the object orientation, so we can’t use new to create objects, we need to create objects by calling of.
In the map method, we also need to replace the new Container with of, because it is static and can be called directly by the class name.
class Container {
static of (value) {
return new Container(value);
}
constructor(value) {
this._value = value;
}
map (fn) {
return Container.of(fn(this._value)); }}let r = Container.of(5);
Copy the code
Notice that we get the functor object, not the value inside the functor, we never get the value inside the functor, if we want to do something with that value, we call the map method, and if we want to print the value, we print it inside the function passed by the map method.
A functor is an object that has a map method, and inside the functor we maintain a value, and that value is never published, like it’s wrapped in a box, and when we want to do something with that value, we call the Map method. The map method returns a new functor after execution.
Functor summary
The operations of functional programming do not operate directly on values, but are performed by functors. A functor is an object that implements the Map contract, that is, all functors have a map object.
We can imagine functor as a box, this box encapsulates a value, if we want to deal with the values in the box, then we will need to box the map method is passed a processing value of the function, this function is pure functions, he needs a parameter and return a value, processing value process to this function.
When the map method is done, it’s going to return a box with the new value, which is a new functor, so we can do a chain call with.map.
Because the map method always returns a functor, and all functors have map methods, and because we can encapsulate different operations into functors, we can derive many different types of functors, as many operations as there are functors, and ultimately use different functors to solve real problems.
One of the problems with the functor we wrote above is that if we pass null when creating the functor, such as if we didn’t get the data from the network request, we might get an error when executing the map method, which would make our function impure.
Since a pure function needs input and output, and null is passed to a function that has no output, the null is actually a side effect, and we need to solve this problem by controlling the side effect.
MayBe functor
In the last summary, when we used Functor, if there was an empty value, there would be an exception, and the MayBe Functor could help us deal with the empty value.
We may encounter a lot of errors in the process of programming, and we need to deal with these errors. The function of the MayBe functor is to deal with external null values.
Passing null values externally can be thought of as a side effect, and the MayBe functor can control this side effect. Now let’s demonstrate the MayBe functor.
Let’s start by creating a MayBe class,
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value;
}
map (fn) {
return MayBe.of(fn(this._value)); }}Copy the code
The MayBe function is used to resolve cases where the value passed in MayBe null. We need to check whether the value is null or undefined before processing it in the map.
We write an auxiliary function to determine if the current value is null, and an isNothing method to determine if this._value has a value.
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value;
}
map (fn) {
return MayBe.of(fn(this._value));
}
isNothing () {
return ths._value === null || this._value === undefined; }}Copy the code
If the current value is null, we cannot call fn. We should return a null functor. If there is a value, we can call fn again.
class MayBe {
static of (value) {
return new MayBe(value)
}
constructor (value) {
this._value = value;
}
map (fn) {
return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value));
}
isNothing () {
return ths._value === null || this._value === undefined; }}Copy the code
If we pass null, our code does not return an error, but instead returns a new MayBe functor with a value of NULL.
Now let’s look at a problem with the MayBe functor. Although we can handle the problem of null values, we are not quite sure which one of the map calls has a null value.
Either functor
The word Either means Either of the two, and when we use Eight, it’s equivalent to if else.
When we used the MayBe functor before, when we passed in null, we didn’t deal with the external function fn. We just returned a functor with a value of NULL, but it didn’t give us any useful information about what went wrong or what went wrong.
We can solve this problem by using the functor Either, which will give us a valid indication when a problem occurs.
The Either functor can also be used to handle exceptions if an exception occurs in a function that makes it impure. Let’s look at the implementation of Either functors.
When we use Either functor, we need to define two types, one for Left and one for Right, in which we need to define static of methods to put back the current object, constructors, and map methods.
In the Left map method, this is a special one that returns this directly. The Right map method is the same as before.
class Left {
static of (value) {
return new Left(value);
}
constructor (value) {
this._value = value;
}
map (fn) {
return this; }}class Right {
static of (value) {
return new Right(value);
}
constructor (value) {
this._value = value;
}
map (fn) {
return Right.of(fn(this._value)); }}Copy the code
If we look at these two functors, we can find that they are basically the same as our previous functors. They both have the of method, constructor and map. In fact, we can inherit the previous Container when writing. Let’s create each functor, print it out and see.
let r1 = Right.of(12).map( x => x + 2); let r12 = Left.of(12).map( x => x + 2); console.log(r1); / /... 14 console.log(r2); / /... 12Copy the code
Here we’ve heard about printing functors created by Left and Right and you can see that Left returns the value we passed in directly without doing anything. The map method in Left returns the current object, this, without calling the currently passed fn.
So why do this? We can embed an error message in Left, so let’s show a function that can go wrong, like we want to convert a JSON string to a JSON object.
Parse can be an exception when calling json. parse, so we use try… The catch. If an exception happens and we don’t handle it it’s not a pure function, and now we want to handle it functionally, so we need some pure function.
Here we need to return a functor in the try, and we’re going to give our converted result to the functor, and we’re going to process it inside the functor, and we’re just going to return the correct value, right.of.
The function we create with right.of, when we call the Map method, the map method passes this function to handle the value we pass. Here we pass the value json.parse (STR) that converts the string to an object.
We also return a value in the catch if there is an error, because pure functions need to have an output, and we also return a functor. The Left in Either is used to handle exceptions.
function parseJSON (str) {
try {
return Right.of(JSON.parse(str));
} cache (e) {
return Left.of({
error: e.message
})
}
}
Copy the code
So we’re done with parseJSON, and that’s how Either handles exceptions.
IO functor
Now that we have a simple idea of functors, we can think of functors as boxes that hold a value. By calling the box’s map method, we can pass in a function that processes the value inside the box.
So now we’re going to look at the IO functor, the input and output functor, and the difference is that the value inside of the functor is always a function.
The IO functor stores all impure operations in value, which stores functions. The function is not called inside the functor. The IO functor delays the execution of these impure operations, which is equivalent to lazy execution.
We use IO functors to wrap some functions and then execute them when we need them, because functions stored in IO functors may be impure, but wrapped in IO functors, our current operation is a pure operation. Impure operations are deferred until called.
IO functors can cage all kinds of impure operations, but these impure operations will eventually be executed, and we can hand these impure operations over to the caller.
When we use an IO functor, we create an IO class, and the constructor takes a function, which is different from that, and we store that function. The of method also takes in data, returns an IO functor in the OF method, and passes in a function in the constructor that returns data.
We can see from the of method that the IO functor ultimately wants to return the data, but it’s wrapped in a function that holds the value of the IO functor. This function returns a value that is deferred and then calls the value function of the IO functor when we want the value.
The map method receives a FN function and creates an IO functor by calling the IO constructor in the map method. In the parameter, fp flowRight is called to combine fn and value, and finally the new function is passed to the IO constructor. Get an IO functor and return.
const fp = require('lodash/fp');
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
consturctor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value)); }}Copy the code
We first call the OF method of IO to return a functor, and the of method receives a value. We can pass in a process(Node environment only).
Next we call the map method to get the exePath in Process, which is the execution path of the process in the current Node environment.
let r = IO.of(process).map(p => p.execPath);
console.log(r);
Copy the code
So what we’re returning here is an IO functor, and the value in that IO functor holds function, so let’s see who that function is.
We pass in the process object when we call IO. Of, we return a functor and wrap process in the function, and then we call the Map method. In the map method we call flowRight to wrap the process function composition in OF over the map function. Returns an IO functor.
So this function is the value of the current functor which is the combined function.
So we want to get the result, we want to call the function in the IO functor, we see that the value in IO is a function, so we can call r._value() directly.
let r = IO.of(process).map(p => p.execPath);
console.log(r._value());
Copy the code
To sum up, the IO functor has some functions wrapped inside it. When we pass the function, it may be an impure operation. We don’t care whether the function is pure or not.
The map method always returns an IO functor, but some of the functions stored in the value property of the IO functor may be impure because many functions will eventually be merged. We delay the impure operation until it is called, which is controlled by the IO functor.
Monad functor
The word Monad means single-celled animal, and we often translate it as Monad.
Before we get to Monad let’s talk about IO functors. In Linux we have a cat command that reads the contents of a file and prints it out. Let’s write a function that emulates this command.
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
consturctor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value)); }}Copy the code
Let’s write a function that reads files, then a function that prints, and then combine them into a function.
Because reading the file has the side effect of making the function impure, we use the IO functor, which means we delay reading the file.
In the print function we also return the IO functor, deferred execution
let readFile = function (filename) {
return new IO(function () {
return fs.readFileSync(filename, 'utf-8'); })}let print = function (x) {
return new IO(function () {
console.log(x);
returnx; })}Copy the code
Now let’s combine these two functions into cat.
let cat = fp.flowRight(print, readFile);
let r = cat('package.json');
console.log(r);
Copy the code
After this call, we return an IO functor from readFile, and when the IO functor is passed to print, the function returns a functor, and the value in the functor is the functor of readFile, so we get a nested functor here.
Now we go to the function inside the functor, which we described earlier can be done by calling _value().
console.log(r._value());
Copy the code
When we execute _value, we get the IO function returned by readFile, because the value returned by readFile is passed to print.
Now that we want to get the result of the file, we need to call the _value method again, which is the _value in readFile
console.log(r._value()._value());
Copy the code
At this point we have the contents of the file, but the problem is that it is very inconvenient to call the nested functor, we need._value()._value(), which looks weird.
Let’s take a look at Monad to solve these problems.
Monad is a conservative functor that can be flattened, so what is flattening? We have a problem above, that is, if functors are nested, it will be very inconvenient to call. Flattening is to solve the problem of functor nesting.
We learned earlier that you can solve this problem by using function composition if functions are nested, and Monad if functors are nested.
A functor is a Monad if it has both join and of methods and complies with some laws.
Of is familiar, join is not complicated, and it returns our call to _value directly.
Let’s change the IO class to Monad and add a join method. The join method doesn’t take any arguments, so here it’s just a call that returns _value.
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
consturctor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value));
}
join () {
return this._value(); }}Copy the code
Then we write a flatMap method. When using Monad, we often combine map and join, because the function of map is to combine the current function with the value inside the functor to return a new functor. When map combines this function, the function will eventually return a functor. So we need to call join to flatten it and pat it flat.
FlatMap calls map and JOIN at the same time. The Map method needs a FN parameter, so flatMap also needs a parameter. After the completion of flapMap, we need to call JOIN, and the result of join execution, That’s what this functor returns.
When we call map, we merge value and fn and return a new functor. The function wrapped in this functor will also return a functor, so we call join() again.
class IO {
static of (x) {
return new IO(function() {
return x;
})
}
consturctor (fn) {
this._value = fn;
}
map (fn) {
return new IO(fp.flowRight(fn, this._value));
}
join () {
return this._value();
}
flatMap(fn) {
return this.map(fn).join(); }}Copy the code
Now that Monad is done, let’s see how to use it.
let r = readFile('package.json');
Copy the code
When we call readFile it generates a functor that wraps around the operation that reads the file, and then we combine the operation that reads the file with the operation that prints it.
Whether we call map or flatMap depends on whether the function we are merging returns a value or a functor. If so, map is called and the functor calls flatMap.
let r = readFile('package.json').flatMap(print);
Copy the code
After we call readFile, we return an IO functor that encapsulates a function that reads the file. Then we call flatMap. We pass print. This. Map is called inside the flatMap, and we merge print with the value inside the current functor, which returns a new functor.
When we call map, we get a functor, and the function that returns the kingdom returns a functor, and then we call Join, which returns the value of the functor.
So flatMap returns the functor of print. Finally, we want to get the file contents of print. We can call join again, because join is calling the internal value.
let r = readFile('package.json').flatMap(print).join();
Copy the code
This may seem cumbersome, but in practice you don’t need to worry about the internal implementation of the functor. You just need to call the functor API to implement the desired function.
Suppose we have read the contents of the file, and we want to convert all the strings of the file to uppercase, we call map directly after readFile, because map handles the value inside the functor.
Task functor
Task functors can help us control side effects, handle exceptions, and handle asynchronous tasks, which can cause callback hell.
Using Task functors avoids the nesting of callbacks because the implementation of asynchronous tasks is too complex, so we will use the Task functors provided in the Folktale library to demonstrate this.
Folktale is a standard functional programming library. Unlike Lodash and Ramda, it does not provide many functional functions. It only provides functions related to functional processing, such as compose, Curry, etc. Besides, it also provides some functors, such as Task, Eight, MayBe, etc.
Let’s take a look at how compose and Curry from Folktale can be used.
const { curry } = require('folktale/core/lambda');
let f = curry(2.(x, y) = > {
return x + y;
});
console.log(f(1.2));
Copy the code
Curry is different from Lodash in that it takes two arguments, the first of which specifies how many arguments the function argument has. The documentation says that the purpose of passing the first parameter here is to avoid some errors.
Now, for compose, which is the composition of functions, instead of writing the function itself, we’re going to use lodash’s function, and we’re going to take the first element in the array and convert it to uppercase.
const { compose } = require('folktale/core/lambda');
const { toUpper, first } = require('lodash/fp');
let f = compose(toUpper, first)
console.log(f(['a'.'b']));
Copy the code
The compose function is the same as the flowRight in LoDash.
Let’s take a look at the Task functors provided in Folktale to handle asynchronous tasks. The use of tasks in folktale2.x is quite different from that in folktale1.x. Just the API is different, we can refer to the documentation to understand the use.
Let’s demonstrate asynchronous tasks by reading a file. We need to refer to the documents to know the specific location of folktale.
The task provided here is a function that returns a functor object, in the case of 1.x, a class.
Next we write a file-reading function called readFile that takes a file path argument and returns a Task functor.
Task accepts a function called resolver, which takes a fixed parameter. Resolver is an object that contains two methods: resolve and reject. It works very much like Promise.
We read the file in this function.
const { task } = require('folktale/concurrency/task');
const fs = require('fs');
function readFile(filename) {
return task(resolver= > {
fs.readFile(filename, 'utf-8'.(err, data) = > {
if (err) {
resolver.reject(err);
} else {
resolver.resolve(data)
}
})
})
}
Copy the code
When we call the readFile function, it returns a Task functor. When we want to read the file, we need to call the run method provided by the Task functor.
readFile('package.json').run();
Copy the code
File read status can be monitored using the Listen method, which passes in an object containing the onRejected callback and onResolved callback.
readFile('package.json').run().listen({
onRejected: err= > {
console.log(err);
},
onResolved: value= > {
console.log(value); }})Copy the code
If we want to process the value, we can call the map method of the Task functor before we run. The map method can process the result. This is more functional programming.
In the map method we are going to process the return result of the file that we get, so when we use the functor, we don’t have to think about the implementation mechanism in it. Before we write the functor, we know the internal implementation mechanism, and we use it in the actual development process.
readFile('package.json').map(value= > {
console.log(value); // Process files
retrun value;
}).run().listen({
onRejected: err= > {
console.log(err);
},
onResolved: value= > {
console.log(value); }})Copy the code
Pointed functor
The conservative functor refers to a functor that implements the static of method, so all of our previous functors implemented the OF method, so they are all conservative functors.
The purpose of the of method is to avoid using new to create objects and make our code look object-oriented, but the deeper meaning of the of method is that it is used to put values into a context and then process our values in that context. Put the value into the container and use map to process the value.
Let’s say our value is 2, and we can use the of method to put that value into a box, and that box is what we call the context, which is actually our functor.
So let’s say we have a Container functor, and this functor has an of method, which is a conservative functor, and the of method just wraps the value in a new functor and returns it. Then we call the returned result the context.
When we call the of method we get a context in which we will process the data in the future.
This is the 50% functor. It’s a simple concept that we already use.
conclusion
This is the end of functional programming, let’s summarize, the whole functional programming we divided into four parts.
- Learn about functional programming
That’s the concept of functional programming.
Functional programming is a programming paradigm, or programming ideas, he is the same level and object oriented programming, we want to master a programming thought is takes a long time, we can apply the directly in the work of our master, don’t need to write everything with functional, because it seems too difficult.
The core idea of functional programming is to abstract the operation process into functions, and to program for functions in the process of programming.
The reason we’re learning functional programming is because vue and React already use some of the same ideas internally, so learning functional programming helps us use vue and React.
- Functional review
Functions are first class citizens meaning that functions are also objects, so we can treat functions like values, or functions as arguments to another function, or as return values.
Higher-order functions actually take functions as arguments or functions as return values, and that’s what we do when we use Currization or function combinations, and closures are everywhere.
- Fundamentals of functional programming
Lodash is a functional programming library, it provides a lot of functional programming methods, can assist our development.
Pure function refers to a function input the same parameters, always get the same output, and no side effects, pure function is actually a mathematical function, can map a value to another value, pure function can be cached, testable and convenient parallel processing.
Cremation can reduce the dimension of a function, that is, we can convert a function of several variables into a function of one variable, and the purpose of converting a function of several variables into a function of one variable is to use it when we combine functions.
We can think of a function as a pipeline for processing data. We input data to the pipeline, and when the data passes through the pipeline, it will get a corresponding result. This is how the combination of functions is handled. Function composition Can combine multiple unary functions into a new function, which can be combined into a more powerful function.
- functor
A functor can help us control side effects, do exception handling, do asynchronous operations, etc. The concept of a functor is very simple. We can think of a functor as a box. This box contains a value, and if we want to process this value, we need to call the map method provided by this box.
The map method takes a parameter of a function type, and the function we pass is the one that handles the value.
We used Functor to demonstrate the basic use of Functor, and then we learned about the MayBe Functor. The MayBe Functor helps us handle empty value exceptions. If we want to handle exceptions, we create Either functors, and the value inside these functors holds a value.
Then we create the IO functor, whose value is stored as a function. We use the IO functor to delay the execution of a function. We use the IO functor to control the side effects.
Again behind our learning Task, at the time of learning Task, introduces a folktale of functional programming library, the library does not provide a functional approach, he provides methods are convenient and functional processing, this library also provides some functor, such as Task, the Task is used for asynchronous processing, help with asynchronous tasks.
Monad functors are used to solve the problem of functors nesting. If a functor has a static of method and a join method, it is a Monad.