Je ne sais pas pour vous, mais pour moi il n'y a pas de meilleur début de journée que de se soucier de la programmation. Le sang bouillonne à la vue d'une critique réussie de l'une des langues "audacieuses" utilisées par les plébéiens, tourmentée avec elle pendant la journée de travail entre les visites timides à StackOverflow.
(En attendant, vous et moi n'utilisons que le langage le plus éclairé et des outils sophistiqués conçus pour les mains habiles de maîtres comme nous).
Bien sûr, en tant qu'auteur du sermon, je prends des risques. Vous aimerez peut-être la langue dont je me moque! Une brochure imprudente aurait pu par inadvertance amener sur mon blog une foule furieuse de mobiles avec des fourches et des torches à portée de main.
Afin de me protéger du feu juste et de ne pas offenser vos sentiments (probablement délicats), je vais parler de la langue ...
... qui vient d'arriver. A propos d'une effigie de paille, dont le seul rôle est de brûler les critiques sur le bûcher.
Je sais que cela semble idiot, mais croyez-moi, à la fin nous verrons dont le visage (ou les visages) ont été peints sur une tête de paille.
Nouvelle langue
Ce sera exagéré d'apprendre une langue complètement nouvelle (et excitante) uniquement pour un article de blog, alors disons qu'elle est très similaire à la langue que nous connaissons déjà. Par exemple Javascript. Bretelles et points-virgules bouclés. if
, while
, etc. - Lingua franca de notre foule.
Je n'ai pas choisi JS parce que cet article parle de lui. C'est juste une langue dans laquelle le lecteur moyen est susceptible de se familiariser. Voila:
function thisIsAFunction(){ return "!"; }
Puisque notre animal en peluche est un langage cool (lu - mauvais), il a des fonctions de première classe . Vous pouvez donc écrire quelque chose comme ceci:
C'est l'une des toutes premières fonctionnalités de classe , et comme son nom l'indique, elles sont cool et super utiles. Vous avez probablement l'habitude de transformer les collections de données avec leur aide, mais dès que vous comprenez le concept, vous commencez à les utiliser partout, bon sang.
Peut-être lors des tests:
describe("", function(){ it(" ", function(){ expect("").not.toBe(""); }); };
Ou lorsque vous devez analyser (analyser) les données:
tokens.match(Token.LEFT_BRACKET, function(token){
Ensuite, après avoir accéléré, vous écrivez toutes sortes de bibliothèques et d'applications réutilisables qui tournent autour des fonctions, des appels de fonction, des retours de fonction des fonctions - une cabine fonctionnelle.
traducteur: dans l'original "Functapalooza". Le préfixe -a-palooza est tellement cool que vous souhaitez le partager avec tout le monde.
De quelle couleur est ta fonction?
Et ici les bizarreries commencent. Notre langue a une particularité:
1. Chaque fonction a une couleur.
Chaque fonction - un rappel anonyme ou une fonction régulière avec un nom - est soit rouge soit bleue. Étant donné que la mise en évidence du code dans notre blog ne met pas en évidence la couleur différente des fonctions, admettons que la syntaxe est:
blue*function doSomethingAzure(){
Notre langue n'a pas de fonctions incolores. Vous voulez créer une fonctionnalité? - doit choisir une couleur. Ce sont les règles. Et il y a quelques règles supplémentaires que vous devez suivre:
2. La couleur affecte la façon dont la fonction est appelée
Imaginez qu'il existe deux syntaxes pour appeler des fonctions - «bleu» et «rouge». Quelque chose comme:
doSomethingAzure(...)*blue; doSomethingCarnelian()*red;
Lorsque vous appelez une fonction, vous devez utiliser un appel qui correspond à sa couleur. Si vous n'avez pas deviné - ils ont appelé la fonction rouge avec *blue
après les crochets (ou vice versa) - quelque chose de très grave se produira. Un cauchemar d'enfance oublié depuis longtemps, comme un clown avec des serpents au lieu de mains qui se cachaient sous votre lit. Il va sauter du moniteur et vous sucer les yeux.
Règle stupide, non? Oh, mais encore une chose:
3. Seule la fonction rouge peut provoquer la fonction rouge.
Vous pouvez appeler la fonction bleue depuis le rouge. C'est casher:
red*function doSomethingCarnelian(){ doSomethingAzure()*blue; }
Mais pas l'inverse. Si vous essayez:
blue*function doSomethingAzure(){ doSomethingCarnelian()*red; }
- vous serez visité par l'ancien Clown Spider Maw.
Cela rend plus difficile l'écriture de fonctions supérieures telles que filter()
partir de l'exemple. Nous devons choisir une couleur pour chaque nouvelle fonction et cela affecte la couleur des fonctions que nous pouvons lui transmettre. La solution évidente est de rendre le filter()
rouge. Ensuite, nous pouvons appeler au moins des fonctions rouges, au moins bleues. Mais alors nous nous blessons à propos de la prochaine épine dans la couronne d'épines, qui est la langue donnée:
4. Les fonctions rouges provoquent de la douleur
Nous ne mettrons pas en évidence cette «douleur», imaginons simplement que le programmeur doit sauter à travers le cerceau chaque fois qu'il appelle la fonction rouge. L'appel peut être trop polysyllabique ou vous ne pouvez pas exécuter la fonction dans certaines expressions. Ou vous ne pouvez accéder à la fonction rouge qu'à partir de lignes impaires.
Peu importe ce que c'est, mais si vous décidez de rendre la fonction rouge, tous ceux qui utilisent votre API voudront cracher dans votre café ou faire pire.
La solution évidente dans ce cas est de ne jamais utiliser de fonctions rouges. Faites tout bleu, et vous êtes de retour dans le monde normal, où toutes les fonctions sont de la même couleur, ce qui équivaut au fait qu'elles n'ont pas de couleur et que notre langage n'est pas complètement stupide.
Hélas, les sadiques qui ont développé ce langage (tout le monde sait que les auteurs des langages de programmation sont sadiques, non?) Collez la dernière épine en nous:
5. Certaines des fonctions essentielles du langage sont en rouge.
Certaines fonctions intégrées à la plateforme, fonctions que nous devons utiliser et qui ne peuvent pas être écrites par nous-mêmes, sont disponibles uniquement en rouge. À ce stade, une personne intelligente peut commencer à soupçonner que cette langue nous déteste.
Tout cela est la faute des langages fonctionnels!
Vous pourriez penser que le problème est que nous essayons d'utiliser des fonctions d'ordre supérieur. Si nous arrêtons de jouer avec toutes ces bêtises fonctionnelles et commençons à écrire des fonctions bleues normales du premier ordre (fonctions qui ne fonctionnent pas avec d'autres fonctions - environ Translator), comme prévu par Dieu - nous nous débarrasserons de toute cette douleur.
Si nous n'appelons que des fonctions bleues, nous rendons toutes nos fonctions bleues. Sinon, nous faisons du rouge. Jusqu'à ce que nous créons des fonctions qui acceptent des fonctions, nous n'avons pas besoin de nous soucier du «polymorphisme de la couleur de la fonction» (polychromatique?) Ou de tout autre non-sens.
Mais hélas, les fonctions d'ordre supérieur ne sont qu'un exemple. Le problème se pose chaque fois que nous voulons diviser notre programme en fonctions pour réutilisation.
Par exemple, nous avons un joli petit morceau de code qui, eh bien, je ne sais pas, implémente l'algorithme de Dijkstra sur un graphique représentant à quel point vos relations sociales se mettent la pression les unes sur les autres. (J'ai passé beaucoup de temps à essayer de décider ce que le résultat signifierait. Indésirable transitoire?)
Plus tard, vous deviez utiliser cet algorithme ailleurs. Naturellement, vous encapsulez le code dans une fonction distincte. Appelez-la de l'ancien et du nouveau. Mais de quelle couleur devrait être la fonction? Vous allez probablement essayer de le rendre bleu, mais que se passe-t-il s'il utilise l'une de ces méchantes fonctions "uniquement rouges" de la bibliothèque du noyau?
Disons que le nouvel endroit à partir duquel vous souhaitez appeler la fonction est bleu? Mais maintenant, vous devez réécrire le code d'appel en rouge. Et puis refaites la fonction qui appelle ce code. Ouf De toute façon, vous devrez constamment vous souvenir de la couleur. Ce sera le sable dans votre maillot de bain sur une programmation de vacances à la plage.
Allégorie des couleurs
En fait, je ne parle pas de couleur. Il s'agit d'une allégorie, d'un dispositif littéraire. Putain - ce n'est pas sur les étoiles sur le ventre , c'est sur la course. Vous soupçonnez probablement déjà ...
Fonctions rouges - asynchrone
Si vous programmez en JavaScript ou Node.js, chaque fois que vous définissez une fonction qui appelle une fonction de rappel (rappel) pour «retourner» le résultat, vous écrivez une fonction rouge. Regardez cette liste de règles et notez comment elles s'intègrent dans ma métaphore:
- Les fonctions synchrones renvoient un résultat, les fonctions asynchrones non; en retour, elles appellent un rappel.
- Les fonctions synchrones renvoient le résultat sous forme de valeur de retour, les fonctions asynchrones le renvoient, provoquant le rappel que vous leur avez transmis.
- Vous ne pouvez pas appeler une fonction asynchrone à partir d'une fonction synchrone, car vous ne pouvez pas connaître le résultat tant que la fonction asynchrone n'est pas exécutée ultérieurement.
- Les fonctions asynchrones ne sont pas compilées en expressions en raison de rappels, nécessitent que leurs erreurs soient traitées différemment et ne peuvent pas être utilisées dans un bloc
try/catch
ou dans un certain nombre d'autres expressions qui contrôlent le programme. - tout ce qui concerne Node.js est que la bibliothèque du noyau est tout asynchrone. (Bien qu'ils aient donné en retour et commencé à ajouter des versions de
_Sync()
à beaucoup de choses.)
Quand les gens parlent de «l'enfer du rappel» , ils parlent de la gêne d'avoir des fonctions «rouges» dans leur langue. Quand ils créent 4089 bibliothèques pour la programmation asynchrone (en 2019 déjà 11217 - environ Traducteur), ils essaient de faire face au problème au niveau de la bibliothèque qu'ils ont été coincés avec le langage.
Je promets que l'avenir est meilleur
en traduction: "Je promets que l'avenir est meilleur" le jeu des mots du titre et du contenu de la section est perdu
Les gens de Node.js ont depuis longtemps réalisé que les rappels faisaient mal et cherchaient des solutions. L'une des techniques qui a inspiré de nombreuses personnes est la promises
, que vous connaissez peut-être également dans le futures
surnoms.
en russe IT, au lieu de traduire "promesses" par "promesses", du papier calque de l'anglais - "promesses" a été établi. Le mot "Futures" est utilisé tel quel, probablement parce que les "futures" sont déjà occupés par l'argot financier.
Promis est un wrapper pour le rappel et le gestionnaire d'erreurs. Si vous songez à passer un rappel pour le résultat et un autre rappel pour l'erreur, alors l' future
est l'incarnation de cette idée. Il s'agit d'un objet de base qui est une opération asynchrone.
Je viens de recevoir un tas de libellés fantaisistes et cela peut sembler une excellente solution, mais c'est surtout de l' huile de serpent . Les promesses facilitent vraiment l'écriture de code asynchrone. Ils sont plus faciles à composer en expressions, donc la règle 4 est un peu moins stricte.
Mais pour être honnête, c'est comme la différence entre un coup à l'estomac ou à l'aine. Oui, cela ne fait pas trop mal, mais personne ne sera ravi d'un tel choix.
Vous ne pouvez toujours pas utiliser de promesses avec gestion des exceptions ou autres
gestion des opérateurs. Vous ne pouvez pas appeler une fonction qui renvoie future
partir d'un code synchrone. (vous pouvez , mais le prochain responsable de votre code inventera une machine à remonter le temps, reviendra au moment où vous l'avez fait et vous mettra un crayon dans le visage pour la raison n ° 2).
Les promesses divisent toujours votre monde en deux moitiés asynchrones et synchrones avec toutes les souffrances qui en découlent. Donc, même si votre langue prend en charge les promises
ou les futures
, elle ressemble toujours beaucoup à ma langue.
(Oui, cela inclut même le Dart que j'utilise. Par conséquent, je suis tellement heureux qu'une partie de l'équipe essaie d'autres approches du parallélisme )
lien de projet officiellement abandonné
J'attends une solution
Les programmeurs C # se sentent probablement complaisants (la raison pour laquelle ils deviennent de plus en plus des victimes est que Halesberg et la société saupoudrent tout et saupoudrent le langage de sucre syntaxique). En C #, vous pouvez utiliser le mot clé await
pour appeler une fonction asynchrone.
Cela rend les appels asynchrones aussi faciles que synchrones, avec l'ajout d'un petit mot-clé mignon. Vous pouvez insérer un appel en await
dans des expressions, les utiliser dans la gestion des exceptions, dans le flux d'instructions. Tu peux devenir fou. Attendons la pluie comme de l'argent pour votre nouvel album de rappeur.
L'attente asynchrone est agréable, nous l'ajoutons donc à Dart. Il est beaucoup plus facile d'écrire du code asynchrone avec lui. Mais, comme toujours, il y a un «Mais». Ça y est. Mais ... vous divisez toujours le monde en deux. Les fonctions asynchrones sont désormais plus faciles à écrire, mais ce sont toujours des fonctions asynchrones.
Vous avez encore deux couleurs. L'attente asynchrone résout le problème ennuyeux # 4 - ils rendent l'appel des fonctions rouges pas plus difficile que l'appel des fonctions bleues. Mais le reste des règles est toujours là:
- Les fonctions synchrones renvoient des valeurs, les fonctions asynchrones renvoient un wrapper (
Task<T>
en C # ou Future<T>
dans Dart) autour de la valeur. - Synchrone vient d'appeler, le besoin asynchrone
await
. - En appelant une fonction asynchrone, vous obtenez un objet wrapper lorsque vous voulez vraiment une valeur. Vous ne pouvez pas développer la valeur jusqu'à ce que vous rendiez votre fonction asynchrone et l'appeliez avec
await
(mais consultez le paragraphe suivant). - En plus d'un peu de décoration attendue, au moins nous avons résolu ce problème.
- La bibliothèque de base C # est plus ancienne que l'asynchronie, donc je pense qu'ils n'ont jamais eu ce problème.
Async
vraiment mieux. Je préfère attendre asynchrone aux rappels nus n'importe quel jour de la semaine. Mais nous nous mentons si nous pensons que tous les problèmes sont résolus. Dès que vous commencez à écrire des fonctions d'ordre supérieur ou à réutiliser du code, vous vous rendez compte à nouveau que la couleur est toujours là, saignant à travers tout votre code source.
Quelle langue n'est pas la couleur?
JS, Dart, C # et Python ont donc ce problème. CoffeeScript et la plupart des autres langages se compilent également en JS (et Dart hérité). Je pense que même ClojureScript a ce piège, malgré leurs efforts actifs avec core.async
Vous voulez savoir lequel ne fonctionne pas? Java Ai-je raison À quelle fréquence dites-vous: «Oui, Java seul le fait correctement»? Et c'est arrivé. Pour leur défense, ils essaient activement de corriger leur oubli en promouvant les futures
et les IO asynchrones. C'est comme une course pire que pire.
tout est déjà en Java
C #, en fait, peut également contourner ce problème. Ils ont choisi d' avoir de la couleur. Avant d'ajouter async-wait et toutes ces ordures Task<T>
, vous pouvez simplement utiliser des appels API synchrones réguliers. Trois autres langues qui n'ont pas de problème de «couleur»: Go, Lua et Ruby.
Devinez ce qu'ils ont en commun?
Streams. Ou plus précisément: de nombreuses piles d'appels indépendants pouvant basculer . Ce ne sont pas nécessairement des threads du système d'exploitation. Les coroutines dans Go, les coroutines dans Lua et les fils dans Ruby sont tous adéquats.
(C'est pourquoi il y a cette petite mise en garde pour C # - vous pouvez éviter la douleur asynchrone en C # en utilisant des threads.)
Mémoire des opérations passées
Le problème fondamental est "comment continuer à partir du même endroit lorsque l'opération (asynchrone) est terminée"? Vous avez plongé dans l'abîme de la pile des appels , puis appelé une sorte d'opération d'E / S. Pour des raisons d'accélération, cette opération utilise l'API asynchrone sous-jacente de votre système d'exploitation. Vous ne pouvez pas attendre qu'il se termine. Vous devez revenir à la boucle d'événements de votre langue et donner au système d'exploitation le temps de terminer l'opération.
Une fois que cela se produit, vous devez reprendre ce que vous faisiez. Habituellement, la langue «se souvient de l'endroit où elle était» via la pile d'appels . Il suit à travers toutes les fonctions qui ont été appelées en ce moment, et regarde où le compteur de commandes dans chacun d'eux montre.
Mais pour effectuer des E / S asynchrones, vous devez vous détendre, supprimer la pile d'appels entière en C. Type Trick-22. Vous avez des E / S super rapides, mais vous ne pouvez pas utiliser le résultat! Toutes les langues avec des E / S asynchrones sous le capot - ou, dans le cas de JS, la boucle d'événements du navigateur - sont obligées de gérer cela d'une manière ou d'une autre.
Node, avec ses rappels en marche pour toujours, place tous ces appels dans des fermetures. Lorsque vous écrivez:
function makeSundae(callback) { scoopIceCream(function (iceCream) { warmUpCaramel(function (caramel) { callback(pourOnIceCream(iceCream, caramel)); }); }); }
Chacune de ces expressions fonctionnelles ferme tout son contexte environnant. Cela transfère des paramètres, tels que iceCream
et caramel
, de la pile d'appels vers le tas . Quand une fonction externe renvoie un résultat et que la pile d'appels est détruite, c'est cool. Les données sont toujours quelque part sur le tas.
Le problème est que vous devez ressusciter chacun de ces fichus appels. Il y a même un nom spécial pour cette conversion: style passant-continuation
lier une fonctionnalité féroce
Cela a été inventé par les hackers linguistiques dans les années 70, comme représentation intermédiaire à utiliser sous le capot des compilateurs. C'est une façon très bizarre d'introduire du code qui facilite l'exécution de certaines optimisations du compilateur.
Personne n'a jamais pensé qu'un programmeur pourrait écrire un tel code . Et puis Node est apparu, et soudain, nous prétendons tous écrire un backend de compilateur. Où avons-nous tourné dans le mauvais sens?
Notez que les promesses et les futures
n'aident pas vraiment beaucoup. Si vous les utilisez, vous savez que vous accumulez toujours des couches géantes d' expressions fonctionnelles . Vous les passez simplement à .then()
au lieu de la fonction asynchrone elle-même.
En attente d'une solution générée
L'attente asynchrone aide vraiment . Si vous regardez sous le capot du compilateur lorsqu'il await
, vous verrez qu'il effectue réellement la conversion CPS. C'est pourquoi vous devez utiliser l' await
en C # - c'est un indice pour le compilateur - "arrêtez la fonction au milieu ici". Tout après await
devient une nouvelle fonction que le compilateur synthétise en votre nom.
C'est pourquoi async-wait n'a pas besoin de prise en charge d' exécution dans le cadre .NET. Le compilateur compile cela en une chaîne de fermetures associées, qu'il sait déjà gérer. (Fait intéressant, les fermetures ne nécessitent pas non plus de prise en charge d'exécution. Elles sont compilées en classes anonymes. En C #, les fermetures ne sont que des objets.)
Vous vous demandez probablement quand je parle des générateurs. Y a-t-il du yield
dans votre langue? Ensuite, il peut faire quelque chose de très similaire.
(Je crois que les générateurs et l'attente asynchrone sont en fait isomorphes. Quelque part dans les coins et recoins poussiéreux de mon disque dur se trouve un morceau de code qui implémente la boucle de jeu sur les générateurs utilisant uniquement l'attente asynchrone.)
Alors où suis-je? Ah oui. Ainsi, avec les rappels, les promesses, l'attente asynchrone et les générateurs, vous finissez par prendre votre fonction asynchrone et la diviser en un tas de fermetures qui vivent sur le tas.
Votre fonction appelle external lors de l'exécution. Lorsque la boucle d'événement ou l'opération d'E / S est terminée, votre fonction est appelée et continue à l'endroit où elle se trouvait. Mais cela signifie que tout ce qui est au-dessus de votre fonction devrait également revenir. Vous devez toujours restaurer la pile entière.
C’est de là que vient la règle «il est possible d’appeler la fonction rouge uniquement à partir de la fonction rouge». Vous devez enregistrer l'intégralité de la pile d'appels dans des fermetures vers main()
- main()
ou le gestionnaire d'événements.
Implémentation de la pile d'appels
Mais en utilisant des threads (niveau vert ou OS), vous n'avez pas besoin de le faire. Vous pouvez simplement mettre en pause l'intégralité du thread et passer au système d'exploitation ou à la boucle d'événements sans avoir à revenir de toutes ces fonctions .
La langue de Go, à ma connaissance, le fait parfaitement. Dès que vous effectuez une opération d'E / S, Go garera cette coroutine et continuera toute autre qui n'est pas bloquée par les E / S.
Si vous regardez les opérations d'E / S dans la bibliothèque standard de Golang, elles semblent synchrones. En d'autres termes, ils fonctionnent et retournent le résultat lorsqu'ils sont prêts. Mais cette synchronisation ne signifie pas la même chose qu'en Javascript. Go- , IO . Go .
Go — , . , , .
, API, , . .
, , . , . , 50% .
, , , .
Javascript -, , , JS , JS , . , JS .
, ( ) — , , , async
. import threading
( , AsyncIO, Twisted Tornado, ).
, , , , , , .
, Go, Go .
, , , ( - ) , "async-await ". .
, .
, , .