What color is your function?

I don’t know about you, but for me there is no better start to the day than to bother about programming. Blood boils at the sight of a successful criticism of one of the "bold" languages ​​used by the plebeians, tormented with it during the working day between shy visits to StackOverflow.







(In the meantime, you and I use only the most enlightened language and sophisticated tools designed for the dexterous hands of masters like us).







Of course, as the author of the sermon, I take risks. You may like the language I make fun of! A reckless pamphlet might have inadvertently attracted to my blog a furious mob of mobiles with pitchforks and torches at the ready.







To protect myself from righteous fire and not offend your (probably delicate) feelings, I will talk about the language ...







... who just came up. About a straw effigy, whose only role is to burn down at the stake of critics.







I know this sounds silly, but believe me, in the end we will see whose face (or faces) were painted on a straw head.







New language



It will be an overkill to learn a completely new (and sucky) language only for a blog article, so let's say that it is very similar to the language we already know. For example Javascript. Curly braces and semicolons. if



, while



, etc. - Lingua franca of our crowd.







I chose JS not because this article is about him. It’s just a language that the average reader is likely to get into. Voila:







 function thisIsAFunction(){ return "!"; }
      
      





Since our stuffed animal is a cool (read - bad) language, it has first-class functions . So you can write something like this:







 //  ,     , //    function filter(collection, predicate) { var result = []; for (var i = 0; i < collection.length; i++) { if (predicate(collection[i])){ result.push(collection[i]); } } return result; }
      
      





This is one of those first class features, and as the name suggests, they are cool and super-useful. You are probably used to transforming data collections with their help, but as soon as you grasp the concept, you start using them everywhere, damn it.







Maybe in tests:







 describe("", function(){ it(" ", function(){ expect("").not.toBe(""); }); };
      
      





Or when you need to parse (parse) data:







 tokens.match(Token.LEFT_BRACKET, function(token){ // Parse a list literal... tokens.consume(Token.RIGHT_BRACKET); });
      
      





Then, after overclocking, you write all kinds of cool reusable libraries and applications that revolve around functions, function calls, function returns from functions - a functional shed.







translator: in the original "Functapalooza". The prefix -a-palooza is so cool that you want to share it for everyone.


What color is your function?



And here strange things begin. Our language has one peculiar feature:







1. Each function has a color.



Each function — an anonymous callback or a regular function with a name — is either red or blue. Since the highlighting of the code in our blog does not highlight the different color of the functions, let's agree that the syntax is:







 blue*function doSomethingAzure(){ //   ... } red*function doSomethingCarnelian(){ //    ... }
      
      





Our language does not have colorless functions. Want to make a feature? - must choose a color. These are the rules. And there are a few more rules that you must follow:







2. Color affects the way the function is called



Imagine that there are two syntaxes for calling functions - “blue” and “red”. Something like:







 doSomethingAzure(...)*blue; doSomethingCarnelian()*red;
      
      





When you call a function, you must use a call that matches its color. If you haven’t guessed - they called the red function with *blue



after the brackets (or vice versa) - something very bad will happen. A long-forgotten childhood nightmare, such as a clown with snakes instead of hands that was hiding under your bed. He will jump out of the monitor and suck your eyes.







Stupid rule, right? Oh, but one more thing:







3. Only the red function can cause the red function.



You can call the blue function from red. This is kosher:







 red*function doSomethingCarnelian(){ doSomethingAzure()*blue; }
      
      





But not the other way around. If you try:







 blue*function doSomethingAzure(){ doSomethingCarnelian()*red; }
      
      





- you will be visited by the old Clown Spider Maw.







This makes it harder to write higher functions such as filter()



from the example. We must choose a color for each new function and this affects the color of the functions that we can pass to it. The obvious solution is to make filter()



red. Then we can call at least red, at least blue functions. But then we get hurt about the next thorn in the crown of thorns, which is the given language:







4. Red functions cause pain



We will not precisely define this “pain”, just imagine that the programmer must jump through the hoop every time he calls the red function. The call may be too polysyllabic or you cannot run the function inside some expressions. Or you can only access the red function from odd lines.







It doesn't matter what it is, but if you decide to make the function red, everyone who uses your API will want to spit in your coffee or do something worse.







The obvious solution in this case is to never use red functions. Just make everything blue, and you are back in the normal world, where all the functions are the same color, which is equal to the fact that they have no color and that our language is not completely dumb.







Alas, the sadists who developed this language (everyone knows that the authors of programming languages ​​are sadists, right?) Stick the last thorn in us:







5. Some of the core functions of the language are red.



Some functions built into the platform, functions that we need to use that cannot be written by ourselves, are available only in red. At this point, an intelligent person may begin to suspect that this language hates us.







This is all the fault of functional languages!



You might think that the problem is that we are trying to use higher order functions. If we just stop fooling around with all this functional nonsense, and start writing normal blue first-order functions (functions that do not operate with other functions - translator's note), as planned by God, we will get rid of all this pain.







If we call only blue functions, we make all our functions blue. Otherwise, we make red. Until we create functions that take functions, we don’t need to worry about “polymorphism to the color of the function” (polychromatic?) Or other nonsense.







But alas, higher-order functions are just one example. The problem arises every time we want to break our program into functions for reuse.







For example, we have a nice little piece of code that, well, I don’t know, implements Dijkstra’s algorithm over a graph representing how much your social connections put pressure on each other. (I spent a lot of time trying to decide what the result would mean. Transitive unwantedness?)







Later you needed to use this algorithm somewhere else. Naturally, you wrap the code in a separate function. Call her from the old place and from the new. But what color should the function be? You will probably try to make it blue, but what if it uses one of these nasty "only red" functions from the kernel library?







Let's say that the new place from which you want to call the function is blue? But now you need to rewrite the calling code in red. And then redo the function that calls this code. Phew You will have to constantly remember the color anyway. It will be the sand in your swimming trunks on a beach vacation programming.







Color allegory



In fact, I'm not talking about color. This is an allegory, a literary device. Fuck - this is not about the stars on the tummies , this is about the race. You probably already suspect ...







Red Functions - Asynchronous



If you program in JavaScript or Node.js, each time you define a function that calls a callback function (callback) to “return” the result, you write a red function. Look at this list of rules and notice how they fit into my metaphor:







  1. Synchronous functions return a result, asynchronous functions do not; in return, they call a callback.
  2. Synchronous functions return the result as a return value, asynchronous functions return it, causing the callback that you passed to them.
  3. You cannot call an asynchronous function from a synchronous function because you cannot know the result until the asynchronous function is executed later.
  4. Asynchronous functions are not compiled into expressions due to callbacks, require their errors to be handled differently, and cannot be used in a try/catch



    block or in a number of other expressions that control the program.
  5. the whole thing about Node.js is that the kernel library is all asynchronous. (Although they donate back and started adding _Sync()



    versions to a lot of things.)


When people talk about “callback hell” , they talk about how annoying it is to have “red” functions in their language. When they create 4089 libraries for asynchronous programming (in 2019 already 11217 - approx.Translator), they try to cope with the problem at the library level that they have been stuck with the language.







I promise the future is better



in translation: "I promise that the future is better" the play on words from the title and contents of the section is lost

People at Node.js have long realized that callbacks hurt, and were looking for solutions. One of the techniques that has inspired many people is promises



, which you may also know by the nickname futures



.







in Russian IT, instead of translating "promises" as "promises", a tracing-paper from English - "promises" was established. The word "Futures" is used as it is, probably because the "futures" are already occupied by financial slang.

Promis is a wrapper for callback and error handler. If you are thinking of passing a callback for the result and another callback for the error, then the future



is the embodiment of this idea. This is a basic object that is an asynchronous operation.







I just got a bunch of fancy wordings and it may sound like a great solution, but mostly it's snake oil . Promises really make writing asynchronous code a little easier. They are easier to compose into expressions, so rule number 4 is a little less stringent.







But, to be honest, it's like the difference between a stab in the stomach or groin. Yes, it doesn’t hurt so much, but no one will be delighted with such a choice.







You still cannot use promises with exception handling or other

managing operators. You cannot call a function that returns future



from synchronous code. (you can , but then the next maintainer of your code will invent a time machine, return at the moment you did it, and stick a pencil in your face for reason # 2.)







Promises still divide your world into asynchronous and synchronous halves with all the ensuing suffering. So even if your language supports promises



or futures



, it still looks a lot like my stuffed one.







(Yes, this even includes the Dart that I use. Therefore, I am so glad that part of the team is trying other approaches to parallelism )







project link officially abandoned


I'm awaiting a solution



C # programmers probably feel complacent (the reason they are becoming more and more victims is that Halesberg and company all sprinkle and sprinkle the language with syntactic sugar). In C #, you can use the await



keyword to call an asynchronous function.







This makes making asynchronous calls as easy as synchronous, with the addition of a cute little keyword. You can insert an await



call in expressions, use them in exception handling, and in thread instructions. You can go crazy. Let await's rain like bucks for your new rapper album.







Async-await is nice, so we add it to Dart. It is much easier to write asynchronous code with it. But, as always, there is one "But." Here it is. But ... you still divide the world in half. Asynchronous functions are now easier to write, but they are still asynchronous functions.







You still have two colors. Async-await solve the annoying problem # 4 - they make calling red functions no more difficult than calling blue ones. But the rest of the rules are still here:







  1. Synchronous functions return values, asynchronous functions return a wrapper ( Task<T>



    in C # or Future<T>



    in Dart) around the value.
  2. Synchronous just called, asynchronous need await



    .
  3. By calling an asynchronous function, you get a wrapper object when you really want a value. You cannot expand the value until you make your function asynchronous and call it with await



    (but see the next paragraph).
  4. In addition to a little await decoration, at least we solved this problem.
  5. The C # core library is older than asynchrony, so I think they never had this problem.


Async



really better. I would prefer async-await to naked callbacks any day of the week. But we lie to ourselves if we think that all problems are resolved. As soon as you start writing higher-order functions, or reusing code, you again realize that the color is still there, bleeding through your entire source code.







Which language is not color?



So JS, Dart, C # and Python have this problem. CoffeeScript and most of the other languages ​​compiling in JS too (and Dart inherited). I think even ClojureScript has this catch, despite their active efforts with core.async







Want to know which one doesn't? Java I'm right? How often do you say “yes, Java alone is doing it right”? And so it happened. In their defense, they are actively trying to correct their oversight by promoting futures



and async IO. It’s like the “who’s worse” race.







everything is already in Java

C #, in fact, can also get around this problem. They chose to have color. Before they added async-await and all this Task<T>



junk, you could just use regular synchronous API calls. Three other languages ​​that do not have a “color” problem: Go, Lua, and Ruby.







Guess what they have in common?







Streams. Or more precisely: many independent call stacks that can switch . These are not necessarily threads of the operating system. Coroutines in Go, coroutines in Lua, and threads in Ruby are all adequate.







(That's why there is this small caveat for C # - you can avoid the asynchronous pain in C # by using threads.)







Memory of past operations



The fundamental problem is "how to continue from the same place when the (asynchronous) operation is completed"? You plunged into the abyss of the call stack and then called up some kind of I / O operation. For the sake of acceleration, this operation uses the underlying asynchronous API of your OS. You cannot wait for it to complete. You must return to the event loop of your language and give the OS time to complete the operation.







Once this happens, you need to resume what you were doing. Usually the language “remembers where it was” through the call stack . He follows through all the functions that have been called at the moment, and looks where the command counter in each of them shows.







But in order to perform asynchronous I / O, you must unwind, discard the entire call stack in C. Type Trick-22. You have super fast I / O, but you cannot use the result! All languages ​​with asynchronous I / O under the hood - or, in the case of JS, the browser event loop - are forced to somehow handle this.







Node, with his right-forever marching callbacks, stuffs all these calls into closures. When you write:







 function makeSundae(callback) { scoopIceCream(function (iceCream) { warmUpCaramel(function (caramel) { callback(pourOnIceCream(iceCream, caramel)); }); }); }
      
      





Each of these functional expressions closes its entire surrounding context. This transfers parameters, such as iceCream



and caramel



, from the call stack to the heap . When an external function returns a result and the call stack is destroyed, that's cool. Data is still somewhere on the heap.







The problem is that you have to resurrect each of these damn calls again. There is even a special name for this conversion: continuation-passing style







link fierce functionality

This was invented by language hackers in the 70s, as an intermediate representation for use under the hood of compilers. This is a very bizarre way to introduce code that facilitates some compiler optimizations.







No one ever thought that a programmer could write such code . And then Node appeared, and suddenly we all pretend to write a compiler backend. Where did we turn the wrong way?







Note that promises and futures



do not really help much. If you use them, you know that you are still piling up giant layers of functional expressions . You just pass them to .then()



instead of the asynchronous function itself.







Awaiting a generated solution



Async-await really helps. If you look under the hood to the compiler when it encounters await



, you will see that it actually performs the CPS conversion. This is why you need to use await



in C # - this is a hint to the compiler - "stop the function here in the middle." Everything after await



becomes a new function that the compiler synthesizes on your behalf.







This is why async-await does not need runtime support inside the .NET framework. The compiler compiles this into a chain of related closures, which it already knows how to handle. (Interestingly, closures also do not require runtime support. They are compiled into anonymous classes. In C #, closures are just objects.)







You are probably wondering when I mention the generators. Is there yield



in your language? Then he can do something very similar.







(I believe that generators and async-await are actually isomorphic. Somewhere in the dusty nooks and crannies of my hard drive is a piece of code that implements a game loop on generators using only async-await.)







So where am I? Oh yes. So with callbacks, promises, async-await and generators, you end up taking your asynchronous function and breaking it into a bunch of closures that live on the heap.







Your function calls external at runtime. When the event loop or I / O operation is completed, your function is called and continues from where it was. But this means that everything on top of your function should also return. You still need to restore the entire stack.







This is where the rule comes from: “You can only call the red function from the red function” You must save the entire call stack in closures to main()



or the event handler.







Call stack implementation



But using threads ( green or OS level), you do not need to do this. You can simply pause the entire thread and jump to the OS or the event loop without having to return from all these functions .







The Go language, in my understanding, does this most perfectly. As soon as you do any I / O operation, Go will park this coroutine and continue any other that is not blocked by I / O.







If you look at the I / O operations in the Golang standard library, they seem synchronous. In other words, they just work and then return the result when ready. But this synchronization does not mean the same as in Javascript. Another Go code may work while we wait for an IO operation. So Go eliminated the distinction between synchronous and asynchronous code.







Concurrency in Go is how you choose to model your program, not the color of each function in the standard library. This means that all the pain of the five rules that I mentioned is completely and completely eliminated.







So the next time you decide to tell me a story about a new popular language and its cool asynchronous API, you'll know why I'm starting to grit my teeth. Because we are back to our red and blue sheep.










From translator



, , . , . , 50% .







, , , .







Javascript -, , , JS , JS , . , JS .







, ( ) — , , , async



. import threading



( , AsyncIO, Twisted Tornado, ).







, , , , , , .







, Go, Go .







, , , ( - ) , "async-await ". .







, .







, , .








All Articles