📜 ⬆️ ⬇️

Asynchronous JavaScript: without callbacks and promises

Probably everyone who has used JavaScript has ever encountered (or will face in the future) asynchronous calls. Maybe this will be an appeal to the database on the server side. Maybe - work with a timer to create animation on the site.

In order to "overcome" asynchrony, different tools are used from promises to changing the programming language. But sometimes you really want to drop everything and write linear code on pure JS:

timeout(1000); console.log('Hello, world!'); 

')
Is it possible to implement something like that? Of course, you can.
In this article we will consider one dangerous, but effective way.

Options for action


If you are unhappy with the collectives, you can find several ways of development:

Of course, we will not consider the first and second options, leaving them to life on the conscience of the reader. The third option is more interesting: we don’t seem to write in JS, but despite everything, despite all our naive expectations, the output is a code on it. This suggests: "And let me expand JS, add async operator and call AJS?"
The implementation of such a solution leads to the addition of an unnecessary entity - the compiler of a new language. At the same time, the author will have to advertise well his New Innovative Product and force the public to install another compiler, as well as introduce another explicit code conversion into the development process. If the author does not represent the interests of large companies and is not a recognized authority of his time, nothing will be done.

But wait, you can always pull yourself by the hair! Dynamic language allows us to compile anything on the go, it remains only to determine the convenient form for the programmer to write.

Record form selection


As a first approximation, you can write asynchronous code using string literals, process it, and call eval. True, this is not very different in bulkiness from a stack of callbacks. A little thought, you can use the comments inside the functions. The toString method applied to the function returns us its source code as a string. With the implementation of multi-line lines inside the comments you will not surprise anyone. Depending on the author’s wish, adding a couple of lines of code removes or does not remove spaces at the beginning or line breaks. Using this technology, you can, for example, implement multi-line regular expressions with comments or an interpreter of some language like Brainfuck or JavaScript itself, just add a couple more lines .
Multiline regular expressions
 function createRegExp(func){ if(typeof func !== 'function') throw new TypeError(func + ' is not a function'); var m = func.toString(). replace(/\s+|--.*?((?=\*\/)|$)/gm, ''). //       /* */,    -- match(/^.*?\/\*\/((?:\\\/|.)*?)\/(?:([img]+?)\/?)?\*\/\}$/); //    if(!m) throw new TypeError('Invalid RegExp format'); return new RegExp(m[1], m[2] || undefined); } 


And for example - parsing a regular expression to parse regular expressions in the comments of a multi-line regular expression:

 var re = createRegExp(function(){/* / ^.*?\/\* -- -        \/ --  "/" -    ( (?: \\\/ | . )*? )--     "\/"   , \/ --    "/" (?:([img]+?)\/?)? --      i, m, g, ,   "/", --   \*\/\}$ --        --     / */}); 


Note that the sequence of "-" characters can still be used in a regular expression by separating hyphens with a space.


Such tricks help when working with strings, but they completely kill syntax highlighting, and it is extremely important for the source code for ULVA. Therefore, the code should be used not commented out, but working. That is, one that can at least be parsed and represented as an AST, otherwise we get an interpreter error.

Designs that we can use

To implement our dialect, while remaining within the framework of the language, we can use:



All this will allow us, having received the source code of the function, to find places where the “cherished” construction is used and replace them with the use of promises / nested functions / sending a complaint about the illegal use of asynchronous calls.
Each design has its pros and cons. For example, you can get a warning or an error about an undefined variable, warnings from static analyzers, etc. But in the end, what to use, the developer decides for himself.

Implementation


For example, choose the option shown in the picture.



In the original function, add the argument __cb - the function to which control will pass after the completion of the last asynchronous operation. Asynchronous calls will be denoted by an arrow (<-), indicating that the variables to the left of it would be nice to put the result of the function to the right. All arrows replace the generation of a call to an enclosed callback; all subsequent code will be "packed" into it. Each return from the function is replaced by a return with a call to the __cb callback.

This will allow us to call asynchronous functions, transfer control to another function and use all the variables created (each new variable in front of the arrow is in the lexical context of the subsequent code or one of its parent contexts). The arrow is a sequence of familiar operators "<" and "-", forming a valid expression comparing two numbers.

For simplicity, consider a stripped-down implementation of the transformation of the source code, in which there is no possibility to transfer the execution context, but already demonstrating the possibilities of the approach, since there is an arrow in it:

 function async(func){ //    "function..." (prefix)   (code) var parsed = func.toString() .match(/(function.*?\{)([\s\S]*)\}.*/); var prefix = parsed[1], code = parsed[2]; //  lines       ,  "<-" //  nends     -   //     //        var lines = ['(' + prefix], nends = 2; //   ... (    \n) code.split('\n').forEach(function(line){ // ... ,     "<-", //   -     if(!/<-/.test(line)) return void lines.push(line, '\n'); //   -          , //         lines var parsed = /([\w\d_$\s,]+)<-(.+)\)/.exec(line); lines.push(parsed[2], ', function(', parsed[1], '){\n'); ++nends; //       }); //        return lines.join('') + Array(nends).join('\n});'); } 


It looks and is recorded quite simply for the created functionality!

Those interested can see a more detailed version that describes the declared transformation.
 function async(func){ if(typeof func != 'function') throw new TypeError('First argument of "async" must be a function.'); //  ,   "function...",    var parsed = func.toString() .replace(/\/\*[\s\S]*?\*\/|\/\/.*$/mg, '') .match(/(function.*?)\((.*?)\)\s*\{([\s\S]*)\}.*/); var prefix = parsed[1], args = parsed[2], code = parsed[3]; //   ,     __cb if(!/^\s*$/.test(args)) args += ','; //      "})" //        , ends  //    var lines = ['(', prefix, '(', args, '__cb', '){'], ends = ['\n})']; code.split('\n').forEach(function(line){ //        line = line.replace(/return\s*(.*?);/, 'return void __cb($1);'); // ,   "<-"    if(!/<-/.test(line)) return void lines.push(line, '\n'); if(/<-.*?<-/.test(line)) throw new Error('"<-" is found more than 1 times in "'+line+'".'); //       var parsed = /([\w\d_$\s,]+)<-(.+)\((.*)\)/.exec(line); if(!parsed) throw new Error('"<-" is used incorrectly in "' + line + '".'); lines.push(parsed[2], '('); if(parsed[3]) lines.push(parsed[3], ', '); lines.push('function(', parsed[1], '){\n'); ends.push('\n});'); }); //     "})" return lines.concat(ends.reverse()).join(''); } 



Using


Suppose that in order to get an avatar in an overly distributed system, by an email address, the server must access one database, retrieve the user ID, then obtain the user data (including the file path) using the identifier from the second database, access the file system and upload the file.



A naive solution looks like:
 function getAvatarData(eMail, callback){ db1.getID(eMail, function(id){ db2.getData(id, function(user){ fs.exists(user.avatarPath, function(exists){ if(!exists) return void callback(null); fs.readFile(user.avatarPath, callback); }); }); }); } 


And with the arrow you can make it more "flat":
 function getAvatarData_src (eMail) { id <- db1.getID(eMail); user <- db2.getData(id); exists <- fs.exists(user.avatarPath); if (!exists) return null; data <- fs.readFile(user.avatarPath); return data; } var getAvatarData = eval(async(getAvatarData_src)); 


results


It turned out that you can quite easily implement your syntactic sugar in JavaScript. We retained syntax highlighting and autocompletion, got rid of the external preprocessor and a lot of callbacks. Perhaps they got a tool for prototyping.
Of course, in the ideal case, you should use the normal JS parser, rather than regular expressions (although the fact of adding functionality in a couple of dozen lines pleases) to rid itself of syntax surprises and correctly handle all situations.

You may have to abandon the minimization of the code, since the construction of "a <- f ()" can be optimized or transformed into another form. Also have to follow the context. As the attentive reader noted, the functions described above return a string, and not a ready-made function. This behavior is chosen because of the possible separation of async and the code that uses it into different files - in this case, eval will not “capture” the desired lexical context of the function; eval needs to be called in the same file as the user code.

Also, the presented implementation does not work correctly with exceptions, but their processing can be easily implemented. And of course, she conflicts with serious bosses and ĂĽber-enterprise-projects.

Therefore, repeat this only at home!

Source: https://habr.com/ru/post/232671/


All Articles