En discutant avec plusieurs personnes, j'ai constaté qu'ils pensaient que JavaScript était encore pénible à utiliser, en particulier en ce qui concerne l'appel d'APIs asynchrones et le "callback hell".
Pour illustrer le sujet, j'ai fait un petit programme qui recherche des totoz sur totoz.eu et affiche leur nom sur le terminal.
Le programme doit :
- Appeler l'API https://totoz.eu/search.xml?terms=plop
- Parser le fichier xml et récupérer les noms
- Afficher les noms sur le terminal
Ecrire le programme ne présente aucune difficulté, mais selon le langage cela peut être un peu pénible.
Avant de vous montrer le programme, un petit rappel sur la concurrence en JavaScript :
- Un programme JavaScript est mono-threadé (il y a des façons de passer outre mais ce n'est pas le modèle "normal" d'utilisation)
- Pour ne pas tout bloquer dès qu'on appelle une fonction qui prend du temps, la plupart des APIs sont asynchrones : elles retournent immédiatement, sans attendre que le traitement soit fini.
- Traditionnellement, pour utiliser le résultat du traitement, il fallait passer un callback à la fonction, qui était exécuté une fois le résultat disponible
- Aujourd'hui, on peut utiliser des fonctions dites asynchrones (mot-clé async), dont la syntaxe ressemble à celle des fonctions normales, mais qui peuvent appeler d'autres fonctions asynchrones et n'exécuter l'instruction suivante qu'une fois le résultat des autres fonctions disponible, tout en rendant le contrôle le temps que les autres fonctions s'exécutent.
Voilà donc le programme (et le projet complet sur github) :
const fetch = require('node-fetch')
const xmljs = require('xml-js')
async function searchTotoz(query) {
const res = await fetch(
'https://totoz.eu/search.xml?terms=' +
encodeURIComponent(query))
const xml = await res.text()
const obj = xmljs.xml2js(xml,{compact: true})
const totozList = obj.totozes.totoz.length ? obj.totozes.totoz : [obj.totozes.totoz]
const totozNamesStr = totozList.map(t => t.name._text).join('\n')
console.log(totozNamesStr)
}
if (process.argv[2]) {
searchTotoz(process.argv[2])
.catch(err => console.error(err.message))
}
else
console.error("Syntax: npm start QUERY")
Et vous, comment écririez-vous ce programme dans votre langage favori (en faisant la requête HTTP de façon asynchrone) ?
# Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à -3. Dernière modification le 30 novembre 2018 à 14:01.
Ce n'est pas
asynchrone(désolé, en vrai ça l'est) ça n'est pas fluide à la lecture, tu mets des await(et tu force le blocage du code en attente de la réponse)(en vrai j'imagine que la VM js va aller lancer d'autres en attendant).La bonne methode serait d'utiliser l'API des promesses, du style:
(non testé)
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 6.
J'ai du mal à me départager quant à ma compréhension de ton commentaire, soit :
async/await
est grosso-modo du sucre syntaxique au dessus des promessesasync
sont plus pénibles à comprendre que l'écriture sous forme de promesses[^] # Re: Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à 2. Dernière modification le 30 novembre 2018 à 15:09.
En réalité je l'ai très bien compris, que c'est juste du sucre syntaxique, mais en vrai c'est pas complètement pareil (tout dépend comment sont utilisés les promesses, chaînées ou non, etc) j'ai juste corrigé mon post entre temps (à force de pratiquer plein de langages différents on fini par s'embrouiller) d'où d'ailleurs mes raturages − n'aimant pas cacher mes erreurs, je préfère les assumer.
Ceci dit, je trouve la solution des promesses bien plus élégante à lire, et je pratique cette tournure plutôt que le await/async. D'un certain côté, une fois que t'as commencé à partir dans le fameux "promise hell", si ton code n'est pas bien structuré, ça devient vraiment un enfer, mais si ton code est bien structuré, c'est très élégant: avec ça tu détecte les conneries architecturales assez rapidement.
Mais les JS-eux adorent await/async, beaucoup n'aiment pas les promises, malgré mon petit caffouillage, ça ne méritait pas forcément de passer dans le négatif, c'est une alternative à la fois tout aussi élégante, mais aussi dans certains cas plus pertinent. De plus, ça force à écrire les fameuses callback dont parle l'auteur, mais avec une variante en utilisant la "arrow syntax" (qui ne propage pas le this dans les callback), qui est courte et élégante.
Avec cette syntaxe, une autre chose que j'aime bien: on a pas besoin de déclarer ses variables (const toto) vu qu'on les récupère directement en paramètre des closures et on peut arriver à un stade où on peut considérer que déclarer des variables est une erreur de programmation: on rajoute des embranchements et la complexité cyclomatique et donc on augmente la probabilité d'avoir des bugs (certains analyseurs statiques d'ailleurs mettent ça en valeur).
Par ailleurs, avec les promesses, on ne mélange pas les scopes, et donc on ne partage pas les variables: on réduit par la même occasion les chances d'écrasement ou d'utilisation de variable accidentels de par cette isolation, le code est potentiellement plus robuste (pas forcément plus robuste à l'exécution, mais plus résistant à l'erreur humaine).
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 4.
Je comprends ton point de vue, mais j'ai toujours trouvé ultra relou de transmettre une variable par le biais des enchaînements de retour de promesses et j'ai très souvent besoin de le faire, et je ne pense pas que c'est nécessairement une mauvaise pratique que d'avoir besoin d'accéder à une variable à plusieurs étages de son traitement asynchrone.
Après, il est vrai que l'isolation "cognitive" du code asynchrone est moins simple avec du async/await.
[^] # Re: Tu tiens pas tes promesses
Posté par barmic . Évalué à 3.
C'est un changement qui peut paraître anodin, voir juste une contrainte, mais c'est bien plus que ça. Tu passe de l'utilisation d'une mémoire partagée à un passage de message entre tes traitements. Ça change pas mal de propriété de ton code (possibilité d'écrire des fonctions pures, robustesse au modèle d'exécution, etc). C'est pour ça qu'à titre perso, je trouve que la petite gène vaut largement les gains.
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 2.
En javascript, si tu fais :
Tu n'as pas gagné l'immutabilité hein.
Dans l'écriture async/await, on a même plus les fonctions de l'écriture traditionnelle des promesses, donc c'est difficile de commencer à parler de fonctions "pures" dans ce cas là.
D'expérience, je trouve qu'à condition de garder les fonctions async/await courtes et avec une bonne séparation des responsabilités, on y gagne plutôt globalement en légèreté d'écriture.
[^] # Re: Tu tiens pas tes promesses
Posté par barmic . Évalué à 2.
Bien sûr que tu n'a pas un langage pur, ce n'est pas le question. Tu as des pratiques qui poussent à écrire des fonctions pures, même si ce n'est pas obligatoire. https://linuxfr.org/users/barmic/journaux/adopter-un-style-de-programmation-fonctionnel
Je n'ai pas compris le rapport avec async/await par contre ?
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 3. Dernière modification le 01 décembre 2018 à 09:40.
Bin notre discussion portait sur le choix (ou non) de la forme async/await par rapport à la forme des promesses traditionnelles.
Lorsqu'on choisit async/await, on a plus vraiment de fonctions, contrairement aux enchaînements de promesses, puisqu'on écrit la chaîne sous forme d'une suite d'instructions "pseudo-synchrones".
Si on choisit tout de même de scinder la logique en plusieurs fonctions, alors oui, il faut s'efforcer d'écrire des fonctions les plus pures possibles.
Du coup, passer à async/await pour des fonctions courtes ne me paraît pas si nocif que ça, tant qu'on respecte des bonnes pratiques qui datent d'avant même l'invention des promesses : complexité cyclomatique faible, nommage expressif, etc.
[^] # Re: Tu tiens pas tes promesses
Posté par barmic . Évalué à 2.
Mon commentaire parler du fait de partager des variable par rapport au fait de s'envoyer des messages.
Je ne comprends pas.
async
, si j'en crois NDM, s'applique uniquement aux fonctions.Mais en vrai ça ne change rien à mon propos. Si plusieurs fil d'exécution qui s’exécutent de manière asyncrhone accèdent aux même variables on est dans un état partagé, alors que si elles se transmettent des variables, on est dans du passage de message et on obtient pleins de propriétés cool. Tu gagne déjà une bonne partie de ces propriétés si tu fais « comme si ».
C'est suffisamment clair qu'il s'agit d'une question qui ne dépend pas de la syntaxe que tu utilise ?
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 4.
Oui, je me suis relu et il manquait clairement quelque chose ; mon commentaire était à sous-entendre dans le contexte d'une fonction asynchrone qui comporte les promesses ou les await.
Dans le cas des promesses, notre fonction F va déclarer autant de fonctions (le plus souvent courtes et anonymes) qu'il y a d'étages dans la promise chain.
Dans le cas des async/await, le code habituellement déclaré dans les fonctions courtes et anonymes se retrouvent juste comme autant d'instructions dans la fonction F.
Ca ne change rien au fait que ta remarque est judicieuse (je suis fan d'Elixir et Elm donc je comprends ce que tu veux dire), même si je pense que dans le cadre d'une utilisation raisonnable d'async/await en JavaScript, ça ne pose pas de problèmes particuliers.
Voici le genre de code sous forme de promesses traditionnelles qu'on retrouvait fréquemment dans notre codebase au boulot :
Voici ce que le même code devient avec async/await :
Franchement, je préfère la deuxième version.
Maintenant, avec une fonction d'une complexité cyclomatique de ouf, des await noyés dans la masse, je dis pas…
[^] # Re: Tu tiens pas tes promesses
Posté par n_e . Évalué à 3.
En quoi utiliser des fonctions avec l'arrow syntax est plus court et élégant que ne pas en utiliser du tout ?
Absolument pas. Non seulement c'est possible de ne pas déclarer de variables temporaires avec async/await, mais surtout en déclarer n'est pas une erreur de programmation et rend le code plus clair.
Quand on écrit
const xml = await res.text()
ouconst totozList = obj.totozes.totoz.length ? obj.totozes.totoz : [obj.totozes.totoz]
, il suffit de lire le nom de la variable pour savoir ce qui est retourné.Je ne vois absolument pas en quoi une structure de type
ajoute quoi que ce soit en complexité par rapport à
f3(f2(f1()))
.Dans l'exemple, tout est déclaré avec le mot-clé const, rien ne peut être écrasé. Par ailleurs rien n'empêche d'appliquer les bonnes pratiques comme donner des noms clairs aux variables et écrire des fonctions courtes en utilisant async/await.
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 2.
Malheureusement en JavaScript, c'est pas aussi simple :
[^] # Re: Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à 2.
Comme dans la pluspart des langages objets, ce n'est pas parce que tu n'as pas le droit d'écraser la variable que tu n'as pas le droit d'utiliser les comportements de l'objet sous-jacent. C'est logique, ce qui pèche ici, c'est que tu utilises un objet mutable.
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 2.
Sauf que je ne suis pas certain que l'utilisation du mot
const
dans d'autre langage OO soit vraiment celle de JavaScript. Ca fait longtemps mais il me semble qu'en C++ par exemple, si on déclare un objet en const, on ne peut pas appeler de méthodes qui modifient cet objet, ou bien en tout cas que c'est une mauvaise pratique.[^] # Re: Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à 2.
En C++ peut être, je connais très mal le langage, mais dans les autres langages objet que je connaisse, le const est souvent sémantiquement identique à celui du JavaScript, quand il existe !
[^] # Re: Tu tiens pas tes promesses
Posté par Guillaume Denry (site web personnel) . Évalué à 3.
Etant donné que Java n'a pas le mot clef
const
, tu fais références à quels langages ? Ca interpelle ma curiosité :)[^] # Re: Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à 1. Dernière modification le 30 novembre 2018 à 17:50.
C#, Scala, Rust à mut/pas mut (fait plus que le const), le D (j'ai triché j'ai regardé sur Wikipedia pour celui là) sûrement d'autres par ailleurs.
EDIT: Java a final, je ne sais plus dans quelle mesure il peut remplacer le const (ça remonte de loin, j'ai pas fait de Java depuis 10 ans) d'ailleurs dans ce cadre il permet aussi des optims de perf sur le code compilé (on voit souvent foreach (final Type maVar in enumerable) {} - je suis un peu flou sur la syntaxe là, ça remonte à loin comme je le dis).
[^] # Re: Tu tiens pas tes promesses
Posté par SpaceFox (site web personnel, Mastodon) . Évalué à 3.
final MaClasse
en Java correspond assez bien auconst
de JS : tu ne peux pas modifier la référence elle-même, mais si l'objet est mutable tu peux le muter (ex : tu ne peux pas écraser une liste par une autre, mais tu peux en modifier le contenu).Idem avec
val
en Kotlin.La connaissance libre : https://zestedesavoir.com
[^] # Re: Tu tiens pas tes promesses
Posté par Christie Poutrelle (site web personnel) . Évalué à 0. Dernière modification le 30 novembre 2018 à 16:39.
Tout dépend des conventions et de l'usage, d'un point de vue syntactique pur, ça rend ce que t'écris plus court, et te permet de te focaliser sur l'essentiel. Dans tous les cas, à un moment ou un autre, tu vas écrire des fonctions, qu'elle soit nommées ou anonymes (en JS elles sont toutes des closures donc je ne distingue pas ici). Donc finalement, parfois c'est plus judicieux de l'écrire à l'endroit où tu l'utilises, et puisqu'elle a un usage unique, tu n'as pas besoin de la nommer, donc moins de surcharge cognitive.
D'un autre côté, dans la sémantique du langage en lui même, la short arrow syntax ne se comporte pas comme les closures avec
function()
ou les fonction nommées, à savoir que dès lors que tu utilises unefunction
(nommée ou non) elle dispose de son proprethis
, elle est un objet, elle a un état: une closure sous la forme d'une short arrow function n'a pas d'état, elle est plus proche du fonctionnel pur. D'ailleurs c'est parfois pratique car dans le contexte d'une instance, lethis
dans ta short arrow function va être celui de l'objet (comme le comportement des closures en PHP, et sûrement dans d'autres langages) - ça te permet de propager des comportements d'objets isolés dans du code événementiel, et dans certains cas, améliore grandement la lecture.Je n'ai pas dit que c'était une erreur dans l'absolu, mais que dans certaines conventions, ça peut être considéré comme tel. Et d'ailleurs, en ce qui concerne "rend le code plus clair" je ne suis foncièrement pas d'accord, ça relève du subjectif, mais passer par moult variable créé une surcharge cognitive, tout dépend de comment le lecteur lit bien entendu.
Dans l'absolu, c'est vrai, après, certains analyseurs statiques vont donner un score plus élevé à la complexité dès lors que tu rajoutes des variables temporaires. Même si sémantiquement c'est exactement la même chose, dans certains langages interprétés l'exécution n'est pas équivalente, même si le résultat est le même (par exemple, le simple fait de créer la variable intermédiaire peut être un overhead, exemple en PHP, et sûrement dans certaines VM JS aussi, sauf si par chance ton code est beaucoup exécuté et que la JIT décide de le compiler autrement). C'est sûrement pour ça que ces analyseurs le mesure d'ailleurs.
En effet,
const
est une bonne pratique, d'ailleurs en Scala une des pratiques couramment admise est que rien ne devrait pas êtreconst
, et je pratique moi même cette règle quand j'écris en TypeScript. Sauf parfois dans le cas d'une variable dans un boubouclefor in
oufor of
, mais je conserve le for uniquement quand ça fait sens d'un point de vue performance ou sémantiquement - comme par exemple avec lesNodeList
en retour d'unquerySelectorAll()
, qui n'est ni unArray
ni finalement vraiment un enumerable, sinonArray.forEach()
est très bien, mais très lent, et malheureusement ne peut pas être utilisé dans certains cas.async/await
c'est très bien, je ne suis pas contre, il ne faut pas le prendre sur ce ton ! J'espère que tu as remarqué que je ponctue énormément ce que j'écris avec "parfois", "dans certains cas", "sous certaines conventions" et j'en passe. D'ailleurs j'utilise beaucoup plus le mot-cléasync
, qui permet d'écrire la fonction asynchrone de façon beaucoup plus claire et beaucoup plus concise, sans avoir besoin de manipuler soit même l'objetPromise
, qui rend le code extrêmement verbeux, surtout en TypeScript avec du typage fort, mais la réciproque, dans mes cas d'usage courants, n'est pas vraie, j'utilise plus souvent explicitementsome_function().then().then().catch()
queawait
.Après, les goûts et les couleurs, mais il ne faut pas s'énerver comme ça !
# Java 11
Posté par barmic . Évalué à 5.
Complètement à l'arrache, mais c'était l'occasion d'utiliser le nouveau client http de Java 11. C'est assez verbeux (oui c'est java) et la gestion d'erreur est pourrie.
# Je pense que l'on tient le nouveau TapTempo
Posté par Enzo Bricolo 🛠⚙🛠 . Évalué à 5.
Préparez vous pour une avalanche de journaux rigolos en guise de calendrier de l'avent …
[^] # Re: Je pense que l'on tient le nouveau TapTempo
Posté par Thomas Douillard . Évalué à 5. Dernière modification le 30 novembre 2018 à 18:11.
Et comme exercice de composition de code, à chaque tap un totoz aléatoire s’affiche !
# Go Go Go :)
Posté par woffer 🐧 . Évalué à 3.
Hello,
Pour moi, le Go est dans son élément ici. Les goroutines vous permettent de ne pas vous occuper de l'aspect asynchrone/synchrone.
Go c'est une application qui tourne avec 10 à 20 Mo d'emprunte mémoire, qui se comporte très bien sur un pod k8s dont les ressources sont inférieures 100 millicore, qui démarre quasi-instantanément, dont le déploiement est simplissime un simple binaire dans un container
from scratch
, dont la taille sera de quelques mega (ici 6.1Mo pour binaire linux amd64).Le tout avec un langage qui reste simple (pour des cas simples), très bien outillé (vet, race …), génial pour la concurrence, la gestion des timeouts/fin de vie (via les context) et qui va utiliser tous les cores disponibles de façon optimale.
Bref, qu'en pensez vous ?
https://play.golang.org/p/ElZ99l4WTd-
[^] # Re: Go Go Go :)
Posté par woffer 🐧 . Évalué à 0.
En relisant, je m'aperçois que je n'ai pas fermé le body de la réponse. Donc voici la version corrigée :
[^] # Re: Go Go Go :)
Posté par barmic . Évalué à 2.
Sauf erreur de ma part ton code est totalement synchrone… Comme l'objectif était de présenter les API asynchrones ça marche moins bien.
Je trouve cette condition horrible à lire :
[^] # Re: Go Go Go :)
Posté par woffer 🐧 . Évalué à 1. Dernière modification le 01 décembre 2018 à 20:12.
C'est justement l'intérêt de Go, qui contrairement à Java par exemple, ne bloque pas les threads systèmes sur les entrées/sorties réseaux. C'est pour cela que Go propose que des clients synchrones d'un point de vue des goroutines dans son SDK.
Je peux te dire que venant du monde Java backend, cela a toujours été l'horreur de configurer la taille des pool de threads (souvent fait au doigt mouillé) d'autant plus quand tu accèdes à des API qui répondent plus moins vite (donc bloquant plus ou moins les threads).
J'aime le Go et son semblant d'aspect synchone car il me permet de lire le code comme un livre. C'est dire naturellement.
[^] # Re: Go Go Go :)
Posté par j_m . Évalué à 2.
Vraiment? Est-ce vrai aussi pour des bibliothèques comme netty qui se présente pourtant comme non-bloquante?
J'étais pourtant sûr du contraire.
[^] # Re: Go Go Go :)
Posté par woffer 🐧 . Évalué à 1. Dernière modification le 02 décembre 2018 à 10:47.
Je suis complètement en accord avec toi sauf malheureusement quand les librairies n'utilisent pas Netty ou d'autres (e.g. Apache Mina). Dans mes précédents développements (je travaille pour un grand telco français), nous devions nous interconnecter avec une plateforme d'accès gérant plusieurs millions de clients. Nous utilisions un client radius en Java qui n'utilisait pas les nio. Donc, nous étions obligés de configurer nos pools de threads pour ne pas dégrader la QoS de notre plate-forme avec des pools de threads vidés car le serveur radius était par moment un peu long à répondre…
Alors qu'en Go, l'asynchronisme (via epoll sous linux) est obligatoire car géré par le runtime de façon transparente.
De plus, comme je l'ai déjà dit, je préfère que le code soit écrit en synchrone qu'en asynchrone car beaucoup plus simple maintenir par la suite surtout par quelqu'un d'autre.
[^] # Re: Go Go Go :)
Posté par j_m . Évalué à 2.
Et si tu avais 2 GET à effectuer en parallèle pour fusioner les résultats à la fin, tu aura bien un élément de syntaxe qui indique qu'on synchronise les résultats j'imagine.
[^] # Re: Go Go Go :)
Posté par woffer 🐧 . Évalué à 1.
Bonne question merci.
Il faut utiliser le principe du fan-in fan-out qui permet de paralléliser N requêtes et les merger voir annuler celles qui restent si une échoue. Go montre toute sa puissance ici.
[^] # Re: Go Go Go :)
Posté par barmic . Évalué à 3.
Beaucoup trop de pub pour un petit bout de code :)
[^] # Re: Go Go Go :)
Posté par woffer 🐧 . Évalué à 1.
Oui sans aucun doute ;)
Disons que c'est plus un partage d'expérience :p
# En javascript
Posté par Napin . Évalué à 0.
Je l'aurais plutot vu comme ca :
Il ne faut pas oublier que l'utilisation de
await
n'empeche pas l'utilisation des blocs.then()
.D'un point de vue purement personnel, je trouve que c'est plus elegant et concis.
Dans ce cas precis : search, then arranger l'affichage, then produire l'affichage.
searchTotoz()
devient egalement plus simple, puisqu'il n'y a plus de gestion de l'affichage ou de la sortie .Mais bon, il y a au moins 230 manieres differentes de le faire (full promises, full await en remplacant le
.catch()
par untry{} catch{}
, l'utilisation du modulehttp
ouhttps
natif de node, separer les trois responsabilitees en trois methodes distinctes, etc…).[^] # Re: En javascript
Posté par Guillaume Denry (site web personnel) . Évalué à 4.
Pas besoin du
return Promise.resolve
ici, puisque la fonction est déclarée commeasync
, elle retournera systématiquement une promesse qui sera résolue avec l'objet retourné.# En python 3.7
Posté par Anonyme . Évalué à 2. Dernière modification le 05 décembre 2018 à 00:09.
En utilisant la lib trio
Pour plus d'intérêt ici, on peut passer plusieurs arguments à rechercher au script qui seront du coups exécutés parallèlement.
Le module fire est pratique pour faire des programmes en cli.
python totoz.py moule pas fraiche
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.