The article was written for jewelers, who, due to the growing popularity of jewelery, had to leave their work and do another thing a little contiguous with their previous one.
Whenever I encounter a new concept in programming, I try to find it reflected in the real world, promises - they are agreements, what is it? I will take the liberty to outrun a promise, as a description of the process in which someone is going to do something in the future for someone. Next, we will try to formalize this description in a broad sense. In our abstract imagination, three sets of objects can arise at once, the first of them is those who promise, the second is what they promise and the third to whom they promise. Already complicated, three sets of objects, what are their relations?, How are they related? some questions. The first thing we can do, to simplify, so combine the first with the third set, since their nature is the same, it can be people, compute nodes, etc ... Within the framework of the specific technological environment under consideration, bundles Node Js, Q library, we can eliminate the united set is closed on itself. Indeed, in our case, we give promises to ourselves when we say that there is something to be fulfilled in the code. In general, problems where there could be a place for two sets at once exist. For example, it is easy to imagine a medium from software nodes interacting with each other, which perform joint computations, and then without two sets in any way. But we will consider promises in the Node.Js environment using the Q library as an example.
So, we agreed that all we can do is to promise ourselves that some set of tasks will be fulfilled. Well, as soon as a set of objects appears, in our case a set of tasks, so we can immediately introduce relationships on this set, as mathematicians do. And here immediately emerges one of the relations, which in any way is suitable for our case is the relation of order, it means that one thing follows the other. And as we will soon see, this relationship is realized through the “then” method of the promise object.
')
Let's go back to the real world, is it possible to promise your program manager something like this:
“Sasha, listen, well, I’ll do the loading module after I make the initialization module?”
It is understood that Sasha will not be satisfied with such promises, he needs to build on something, and not the chain of relationships hanging in the air. Now let's look at promises on the other hand, when we promise we automatically build some future for ourselves, we project our actions into a concrete future, geniuses project it into eternity, well, we will consider the near future, if only because the concepts of calculation also change . And here new questions arise, and how can we imagine a concrete future in the Node Js environment.
Node.js is based on event-oriented and asynchronous (or reactive) programming with non-blocking I / O - this is taken from the wiki. What all this means for us, in terms of promises. Imagine a clock with an arrow, every time when it makes its “tick” control is transferred to our script, and when it does “so” control is transferred to Node, but this does not mean that Node works only in the “so” phase, just in the “ so ”we will assume that he informs us about the results of his work, regarding the asynchronous tasks of input-output assigned to us. We made the first attempt to structure the future, we again divided it into "tick-tack", well, let us again, we entered a relationship of order on the set of ticks. Apparently there is no way to leave him, and thank God, I don’t know how people would then program without a causal link. Turning to the tick-to-t model, we thereby narrow the scope of responsibility by simplifying our model of the future, making our promises more fulfilled.
Now we know that at our disposal in the future there is always a phase of "tick", we are sovereign owners in this future. Giving us control Node hopes that we will load it with puzzles, and we usually do it. As we do, we will start over, there is a start-up script, it starts its execution in the “tick” phase, the result of its execution will be tasks that are formed by the base API Node, for example
fs.readFile(filename, [options], callback)
, if any There will be no tasks, then the program will finish its work. As soon as the task is completed, Node will try to plan the execution of the callback in the next “tick” phase, transferring control to it (callback), and in consequence of that to us. And then we have in it set other tasks Node. And since all the asynchronous API is implemented on Node’s callback, the tasks that we describe are located in them.
And this situation was called “the hell of callback functions”. By the way, discussing certain issues on the forums people are very good friends with this hell, because the style of messaging on forums meets all the standards of the style in question. And why this may be a problem in the code, oh yes, programmers are paid only for the length of the code, nobody will pay for extending it horizontally, or not, perhaps not all programmers have wide screens, something is somehow mercantile. Well, of course, they write about callback hell from the point of view of coding style and omit a little what can be hidden behind such a style.
Let's look at this situation.
a(arg1, function() { var x1; b(arg2, function() { var x2; setInterval(function Task() { console.log('tik'); }, interval); }); })
Here, when we set the Node task at intervals with a period of interval to do something, the entire branch of the call tree, it is also called a chain, a closure, continues to exist in memory until the Task lives and is executed, although it may not be used The variables described above and the arguments x1, x2, arg1, arg2 ... And this already makes you wonder whether it is worth writing such nested tasks. For example, in our case, Task can simply output the word "tick".
But these are questions of javascript, and its nature, this is a super dynamic language, where everything happens at the stage of execution, its interpretation, so of course the structure of the code reflects the semantics of its execution. In static, compiled languages, function variables and arguments are located on the stack or in registers and are immediately released as soon as the function completes its work and there is no such problem. And since if there are situations that can lead to useless memory consumption or loss of performance, then it is better to avoid them, but how?
And here comes the concept of promise, following it can be avoided, for example, the situation described above or, for example, such. Slightly higher, we began to outline our future, we divided it into two phases, and realized that we can draw a tick in the phase, or rather, assign tasks for drawing Node js. But what will happen if we consistently write the code twice in a row, see the code above. That's right, at some intervals Task will be executed two times, but in what sequence? But nobody will tell us exactly that, we can only hope for a certain order, but it is not guaranteed to us. Now we recall that we have introduced into our imagination a relationship of order on a multitude of tasks, and we have every right to demand its execution, and the concept of promise, coupled with then give us that right, with the help of then we can link our tasks into a chain and demand their execution one after another. But let me not give an example here with then, let's start a little with another. And what can be a promise from the point of view of implementation in javascript. And as a set of objects (tasks) to submit within javascript. You did not notice how I implicitly talked about the multitude of objects “as about what we promise” and started talking like a multitude of tasks, and then turned it into many promises. Some kind of duality in the objects of this set is observed. So we are gradually moving to a specific implementation.
In order for us to realize the order relation on the considered set within the programming language, we must endow each element of this set with a dual nature, this is both a promise and a task. Those. here, a task is a set of instructions for execution that has no one any idea about when it will be fulfilled, and the promise is exactly what is responsible for the relationship of the order directly, it is hidden in it the mechanisms for determining the start of the task. A promise is something tangible, like a paper contract, where the conditions for performing tasks are spelled out, and a task is a process, something intangible, their duality can be compared with a pair of space (promise) -time (task). We can compare the space - memory, and time - code, instructions. But javascript contains this duality natively, functions can have properties, as well as object methods. Choose what you like.
The developers of the Q library decided that the best option is to associate the task with a native javascript function, and with a promise an object. How can a simple function in which we encode a task become a task within the framework of some concept, what should we do with it in order to distinguish it from everyone? It is clear that some sort of classification feature should be with her so that the promise technology is applicable to her if this feature does not exist, whereas Q can separate normal functions from tasks. Javascript has enough classification options. But the developers decided to use the function wrapper mechanism, which quite naturally and beautifully fits into the nature of functional javascript. Those. they tell library users, write functions, transfer them to special wrapping functions of the library, and in their context we will consider them as tasks. Those. in fact, we generally can do nothing special in order to begin working with the concept of promise.
That is, in order for us to guarantee an order relation for two tasks, we can write:
Q.fcall(function task1() {…}).then(function task2(){…});
Here, fcall and then are just the wrapper functions, getting into them task1 and task2 become tasks, we didn’t do anything with the functions for this, and they will not acquire any hidden properties in the process of executing the code above. What this code gives us, but it will give us guaranteed execution of task2 for task1, but in the following ticks. By task execution, I mean the execution of a function, i.e. when the return expression or the end of it is reached in the function (the yield statement, I’ll omit here). If, in the functions task1 and task2, there is an API call with asynchronous callbacks, then the execution sequence is not guaranteed by the code, see above, the callback will be on our own if we do not take special actions to do this. Then you ask, why then such a construction is needed, you can simply write:
task1(); task2();
Yes, it is possible, but when one of them, for example, the first one, has inside itself a call to the asynchronous API, and the second does not, then set the order with task1 () code; task2 (); without doing anything inside of them for that, it will not work. Another example, we have the following sequence of tasks A (), B (), C () A and C - asynchronous, and B synchronous, then we can write
Q.fcall(A).then(B).then(C)
while, as we later see in function B, there is no need to return a special object of a promise, the Q library will do it for us.
We now turn to the promises, and where they appear here, where these papers, as they say without pieces of paper, my brother and I are not twins. In the depths of the structure, see above, promises are born, and the result of its execution will be a promise. How this happens inside and how the Q library is implemented will leave it for a while, the most important thing is to understand that the call chain, sometimes called the Q monad of functions (fcall, then, etc.), almost always generates a promise, which in turn can be considered as a link between other monads. The explanation is that in the depths of what is created there is not always enough to fully control the situation, and of course the library provides a mechanism for explicit promise creation.
Is it possible to do without him and why is it needed? If we work with synchronous tasks only, then we obviously do not need to create promises explicitly. If we work with the Node JS asynchronous API, then for it in the Q library there are wrapping functions that implicitly create promises, i.e. There is a theoretical possibility not to use the mechanism of explicit promise creation and even in the case of working with asynchronous tasks. Moreover, this wrapping style applies to all asynchronous functions of other libraries, which include callback functions of the form (error, result) as the last parameter of the asynchronous function.
Here is an example from the Q manual library:
Q.nfapply(FS.readFile, ["foo.txt", "utf-8"]).done(function task (text) { … });
Here you can see how to call the standard function readFile Node JS. wraps into a nfapply call that takes two arguments: the wrapped function and an array of arguments that will be passed to readFile later when called in the next tick. And where did the callback function readFile go? Its place in such a construction is automatically taken by a function, in our example, its name is task, transferred to the method of the promise done object. In some surprising way, Q began to understand that as soon as the asynchronous function performs its task, it will transfer control to our task. And how she does it.
Here is the pseudo-code of what the nfapply function does approximately:
function nfapply(readFile, [args…]) { var deffered = Q.defer(); readFile([args…], deffered .resolver()); return deferred.promise; }
Here a deffered object is created; its resolver method returns a function, let's call it handler, which is substituted for the readFile callback function, a simplified handler looks like this:
function handler(err, result) { if (err) deffered.reject(err); else deffered.resolve(result); }
I deliberately do not touch the issues related to the handling of exceptions, so as not to complicate the basic understanding.
Now it becomes obvious why the wrappers for asynchronous functions impose a restriction on the type of callback function, it is enough to change the err arguments, result in places, how the semantics of the wrapping function will change drastically and then we cannot do without explicitly creating objects to control the process of resolving and rejecting promises completely .
You can create a promise simply by calling the Q.defer () method, which will return the “deferred” object containing the promise object, which we have already seen, see above. And the methods of the object “deferred” allow us to do with the promise all the same things that we do with them in real life, reject -reject, fulfill - resolve, signal on progress - notify. Here you have a question at your leisure, why we cannot promise to postpone within the Q library, i.e. why the suspend method is not implemented.
Let's return to our chains of sequentially executed tasks - functions. Let's imagine it as a racetrack on which competitions in relay race take place. A task is a runner who, running through his distance, passes the flag to another runner, the next task. The checkbox for the idea should reach the finish line. We may well regard the checkbox as a promise that is passed from one task to another, until another task decides not to replace it, or until it reaches the finish line. The substitution takes place in a simple way, just during a runner, the runner pulls out his flag from his pocket and passes it to the next runner, who doesn’t have time to look at him, he just gets it and with him he runs on. Any runner can replace the flag. In order for us to make a substitution in the code, we simply in the function - the task return the object promise. When we do this, the Q library will understand this, and the next task will begin its execution only after we reject or keep our promise, causing respectively to resolve or reject the deferred object. Here is a simple illustration of a basic template:
Q.fcall(function task1() { var deferred = Q.defer(); asyncOp(function callback(result) { … deferred .resolve(result); };) return deferred.promise; }).then(function task2(result){ console.log(result); });
If we understand how to connect two links, then we should easily assemble multi-link chains. But there is one thing here, but not on all the multitude of promising tasks we are considering, in real life a relation of order can act, i.e. for some tasks we can determine the order between them, and for some there isn’t. And here those that do not determine the order in relation to each other can be grouped into groups of independent tasks, they can be carried out in parallel. And for this case, the Q library has constructs that allow you to implement this behavior of things. The Q.all function ([...]) takes as its argument an array of promise objects, giving us a promise that as soon as all of them are executed or at least one of them deviate, she will let us know about it. And she does it simply, returning everything to us, too, the promise (promise object), which we can continue to connect with others, but with decorations. Parallel tasks can be viewed as jewelery on a chain. Now we can make standard jewelry.
Consider the process of putting promises into orbit in more detail, when we write Q.defer () we get the object deferred, why not promise immediately, why we need some kind of auxiliary object, again some kind of duality. When we launch a rocket into orbit, what does it lose in the process of take-off ?, correctly, one stage after another. About the same thing happens here. Made deferred his work, now he can walk boldly. When we create this object, all we need from it is, as a rule, a call to one of its following methods, reject, resolve, which translate our promise into a certain, but already unchanged state. , , , , , . , resolve, reject, , , , Q. , . , deferred.resolve(value) deferred.reject(value) value .
, , , , , nfapply, fcall , , . — , .. .
, - .