HolyJS 2019: débriefing de SEMrush (Partie 2)



Il s'agit de la deuxième partie de l'analyse des tâches de notre stand lors de la conférence HolyJS , tenue à Saint-Pétersbourg du 24 au 25 mai. Pour plus de contexte, il est recommandé de lire d' abord la première partie de ce document. Et si l' expression du compte à rebours est déjà terminée, bienvenue à l'étape suivante.

Contrairement à l'obscurantisme dans la première tâche, les deux suivantes ont déjà une idée de l'applicabilité des applications normales dans la vie. JavaScript se développe encore assez rapidement et les solutions aux problèmes suggérés mettent en évidence certaines des nouvelles fonctionnalités du langage.

Tâche 2 ~ effectuée par les uns


Il a été supposé que le code s'exécuterait et imprimerait les réponses à la console en réponse à trois demandes, puis «terminé». Mais quelque chose s'est mal passé ... Corrigez la situation.

;(function() { let iter = { [Symbol.iterator]: function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); } } }; for (const res of iter) { console.log(res); } console.log('done'); })(); 

Problème de recherche


Qu'avons-nous ici? Il s'agit d'un objet iter itérable qui a un symbole Symbol.iterator bien connu défini par une fonction de générateur . Un tableau fs est déclaré dans le corps de la fonction, dont les éléments tombent dans la fonction fetch à leur tour pour envoyer une requête et le résultat de chaque appel de fonction est retourné via yield . Quelles demandes la fonction d' extraction envoie-t-elle? Tous les éléments du tableau fs sont des chemins relatifs vers les ressources avec les nombres 1, 2 et 3, respectivement. Ainsi, l'URL complète sera obtenue en concaténant location.origin avec le numéro suivant, par exemple:

GET https://www.example.com/1

Ensuite, nous voulons itérer l'objet iter à travers for-of , afin d'exécuter chaque demande tour à tour avec la sortie du résultat, après tout - imprimer «done». Mais ça ne marche pas! Le problème est que l' extraction est une chose asynchrone et renvoie une promesse, pas une réponse. Par conséquent, dans la console, nous verrons quelque chose comme ceci:

Promise {pending}
Promise {pending}
Promise {pending}
done

En fait, la tâche se résume à la réalisation de ces mêmes promesses.

Nous avons asynchrone / attend


La première pensée peut être de jouer avec Promise.all : donnez-lui notre objet itérable , puis la sortie vers la console est «terminée». Mais il ne nous fournira pas l'exécution séquentielle des demandes (comme l'exige la condition), mais les enverra simplement toutes et attendra la dernière réponse avant la résolution générale.

La solution la plus simple ici serait d' attendre dans le for-of body d'attendre la résolution de la prochaine promesse avant de sortir sur la console:

 for (const res of iter) { console.log(await res); } 

Pour attendre que cela fonctionne et que "done" soit affiché à la fin, vous devez rendre la fonction principale asynchrone via async :

 ;(async function() { let iter = { /* ... */ }; for (const res of iter) { console.log(await res); } console.log('done'); })(); 

Dans ce cas, le problème a déjà été résolu (presque):

GET 1st
Response 1st
GET 2nd
Response 2nd
GET 3rd
Response 3rd
done

Itérateur et générateur asynchrones


Nous laisserons la fonction principale asynchrone, mais pour wait il y a une place plus élégante dans cette tâche que dans le corps for-of : c'est l'utilisation de l'itération asynchrone via for-wait-of , à savoir:

 for await (const res of iter) { console.log(res); } 

Tout fonctionnera! Mais si vous passez à la description de cette proposition sur l'itération asynchrone, voici ce qui est intéressant:

Nous introduisons une variation de l'instruction d'itération for-of qui itère sur les objets itérables asynchrones. Les instructions for-of asynchrones ne sont autorisées que dans les fonctions asynchrones et les fonctions du générateur asynchrone

Autrement dit, notre objet doit être non seulement itérable , mais «asyncIterable» via le nouveau symbole bien connu Symbol.asyncIterator et, dans notre cas, déjà une fonction de générateur asynchrone:

 let iter = { [Symbol.asyncIterator]: async function* iterf() { let fs = ['/1', '/2', '/3']; for (const req in fs) { yield await fetch(req); } } }; 

Comment cela fonctionne-t-il alors sur un itérateur et un générateur réguliers? Oui, juste implicitement, comme beaucoup plus dans cette langue. Cette attente est délicate: si l'objet est seulement itérable , alors pendant l'itération asynchrone, il "convertit" l'objet en asyncIterable en encapsulant les éléments (si nécessaire) dans Promise avec l'attente de résolution. Décrit plus en détail dans l'article Axel Rauschmayer .

Probablement, grâce à Symbol.asyncIterator, ce sera toujours plus correct, car nous avons explicitement créé l'objet asyncIterable pour notre itération asynchrone via for-wait-of , tout en laissant la possibilité de compléter l'objet avec un itérateur régulier pour for of , si nécessaire. Si vous voulez lire quelque chose d'utile et suffisant dans un article sur les itérations asynchrones en JavaScript, alors le voici !

Le for-of asynchrone est encore partiellement en projet, mais il est déjà pris en charge par les navigateurs modernes (sauf Edge) et Node.js à partir de 10.x. Si cela dérange quelqu'un, vous pouvez toujours écrire votre propre petit polyphile pour une chaîne de promesses, par exemple, pour un objet itérable :

 const chain = (promises, callback) => new Promise(resolve => function next(it) { let i = it.next(); i.done ? resolve() : i.value.then(res => { callback(res); next(it); }); }(promises[Symbol.iterator]()) ); ;(async function() { let iter = { /* iterable */ }; await chain(iter, console.log); console.log('done'); })(); 

De cette manière et de ces façons, nous avons compris l'envoi des demandes et le traitement des réponses à leur tour. Mais dans ce problème, il y a un autre problème petit mais ennuyeux ...

Test de pleine conscience


Nous étions tellement emportés par tout cet asynchronisme que, comme cela arrive souvent, nous avons perdu de vue un petit détail. Ces demandes sont-elles envoyées par notre script? Voyons le réseau :

GET https://www.example.com/0
GET https://www.example.com/1
GET https://www.example.com/2

Mais nos nombres sont 1, 2, 3. Comme si une décrémentation s'était produite. Pourquoi Juste dans le code source de la tâche, il y a un autre problème avec l'itération, ici:

 let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(req); } 

Ici, un for-in est utilisé , qui au lieu des valeurs de tableau contourne ses propriétés énumérées: et ce sont les indices des éléments de 0 à 2. La fonction fetch les mène toujours à des chaînes et, malgré l'absence de barre oblique avant (ce n'est plus un chemin ), elle résout relativement URL de la page actuelle. Il est beaucoup plus facile de réparer que de remarquer. Deux options:

 let fs = ['/1', '/2', '/3']; for (const req of fs) { yield fetch(req); } let fs = ['/1', '/2', '/3']; for (const req in fs) { yield fetch(fs[req]); } 

Dans le premier, nous avons utilisé le même for-of pour parcourir les valeurs du tableau, dans le second - accès à l'élément du tableau par index.

La motivation


Nous avons envisagé 3 solutions: 1) par attente dans le corps de for , 2) par attente de, et 3) par le biais de notre polyfichier (fonction récursive, pipeline de canalisation, etc.). Il est curieux que ces options aient divisé les participants à la conférence à peu près également et aucun favori évident n'a été révélé. Dans les grands projets, pour de telles tâches réelles, une bibliothèque réactive (par exemple, RxJS ) est généralement utilisée, mais il convient de se rappeler des fonctionnalités natives modernes d'un langage à caractère asynchrone.

Environ la moitié des participants n'ont pas remarqué d'erreurs dans l'itération de la liste des ressources, ce qui est également une observation intéressante. En nous concentrant sur un problème non trivial mais évident, nous pouvons facilement ignorer cette apparence insignifiante, mais avec de graves conséquences potentielles.

Problème 3 ~ Facteur 19


Combien de fois dans le record du nombre 2019! (factorielle à partir de 2019) le nombre 19 se produit-il? Avec la réponse, fournissez une solution JavaScript.

Problème de recherche


Le problème est en surface: nous avons besoin d'un enregistrement d'un très grand nombre pour y trouver le nombre de toutes les occurrences de la sous-chaîne «19». Résolvant le problème sur les nombres , nous avons très rapidement rencontré Infinity (après 170) et n'avons rien obtenu. De plus, le format de représentation des nombres float64 garantit l'exactitude de seulement 15-17 caractères, et nous devons obtenir non seulement un enregistrement complet, mais aussi précis du nombre. La principale difficulté est donc de déterminer la structure d'accumulation de ce grand nombre.

Grands entiers


Si vous suivez les innovations du langage, la tâche est résolue simplement: au lieu du numéro de type , vous pouvez utiliser le nouveau type BigInt (étape 3) , qui vous permet de travailler avec des nombres de précision arbitraires. Avec la fonction récursive classique pour le calcul factoriel et la recherche de correspondances via String.prototype.split, la première solution ressemble à ceci:

 const fn = n => n > 1n ? n * fn(n - 1n) : 1n; console.log(fn(2019n).toString().split('19').length - 1); // 50 

Cependant, deux mille appels de fonction sur la pile peuvent déjà être dangereux . Même si vous apportez la solution à la récursivité de la queue , l' optimisation des appels de queue ne prend toujours en charge que Safari. Le problème factoriel est ici plus agréable à résoudre à travers un cycle arithmétique ou Array.prototype.reduce :

 console.log([...Array(2019)].reduce((p, _, i) => p * BigInt(i + 1), 1n).toString().match(/19/g).length); // 50 

Cela peut sembler une procédure incroyablement longue. Mais cette impression est trompeuse. Si vous estimez, nous avons juste besoin de dépenser un peu plus de deux mille multiplications. À i5-4590 3,30 GHz en chrome, le problème est résolu en moyenne en 4-5 ms (!).

Une autre option pour rechercher des correspondances dans une chaîne avec le résultat d'un calcul est String.prototype.match par expression régulière avec l'indicateur de recherche globale: / 19 / g .

Grande arithmétique


Mais que se passe-t-il si nous n'avons pas encore ce BigInt (et les bibliothèques aussi)? Dans ce cas, vous pouvez faire vous-même la longue arithmétique. Pour résoudre le problème, il nous suffit d'implémenter uniquement la fonction de multiplier grand par petit (nous multiplions par des nombres de 1 à 2019). Nous pouvons contenir un grand nombre et le résultat de la multiplication, par exemple, dans la ligne:

 /** * @param {string} big * @param {number} int * @returns {string} */ const mult = (big, int) => { let res = '', carry = 0; for (let i = big.length - 1; i >= 0; i -= 1) { let prod = big[i] * int + carry; res = prod % 10 + res; carry = prod / 10 | 0; } return (carry || '') + res; } console.log([...Array(2019)].reduce((p, _, i) => mult(p, i + 1), '1').match(/19/g).length); // 50 

Ici, nous multiplions simplement la colonne en bits de la fin de la ligne au début, comme on nous l'a enseigné à l'école. Mais la solution nécessite déjà environ 170 ms.

Nous pouvons quelque peu améliorer l'algorithme en traitant plus d'un chiffre dans un enregistrement numérique à la fois. Pour ce faire, nous modifions la fonction et en même temps allons dans les tableaux, afin de ne pas déranger les lignes à chaque fois:

 /** * @param {Array<number>} big * @param {number} int * @param {number} digits * @returns {Array<number>} */ const mult = (big, int, digits = 1) => { let res = [], carry = 0, div = 10 ** digits; for (let i = big.length - 1; i >= 0 || carry; i -= 1) { let prod = (i < 0 ? 0 : big[i] * int) + carry; res.push(prod % div); carry = prod / div | 0; } return res.reverse(); } 

Ici, les grands nombres sont représentés par un tableau, dont chaque élément stocke des informations sur les chiffres chiffres de l'enregistrement numérique, à l'aide de nombre . Par exemple, le nombre 2016201720182019 avec chiffres = 3 sera représenté comme:

'2|016|201|720|182|019' => [2,16,201,720,182,19]

Lors de la conversion en ligne avant une jointure, vous devez vous souvenir des zéros non significatifs. La fonction facteur renvoie la factorielle calculée par une chaîne, en utilisant la fonction mult avec le nombre spécifié de chiffres traités à la fois dans la représentation "massive" du nombre lors du calcul:

 const factor = (n, digits = 1) => [...Array(n)].reduce((p, _, i) => mult(p, i + 1, digits), [1]) .map(String) .map(el => '0'.repeat(digits - el.length) + el) .join('') .replace(/^0+/, ''); console.log(factor(2019, 3).match(/19/g).length); // 50 

L'implémentation «à longueur de genou» via des tableaux s'est avérée plus rapide que via des chaînes, et avec des chiffres = 1, il calcule la réponse déjà en moyenne en 90 ms, chiffres = 3 en 35 ms, chiffres = 6 en seulement 20 ms. Cependant, rappelez-vous qu'en augmentant le nombre de chiffres, nous approchons d'une situation où la multiplication de nombre par nombre "sous le capot" peut être en dehors du coffre-fort MAX_SAFE_INTEGER . Vous pouvez jouer avec ici . Quelle est la valeur maximale de chiffres que nous pouvons nous permettre pour cette tâche?

Les résultats sont déjà assez indicatifs, BigInt est vraiment assez rapide:



La motivation


C'est cool que 2/3 des participants à la conférence aient utilisé le nouveau type BigInt dans la solution (quelqu'un a admis que c'était la première expérience). Le tiers restant des solutions contenait leurs propres implémentations d'arithmétique longue sur des chaînes ou des tableaux. La plupart des fonctions implémentées multipliaient de grands nombres par de grandes, alors que pour une solution, il suffisait de se multiplier par un "petit" nombre et de passer un peu moins de temps. D'accord, utilisez-vous déjà BigInt dans vos projets?

Remerciements


Ces deux jours de conférence ont été très chargés de discussions et d'apprentissage de quelque chose de nouveau. Je remercie le comité de programme pour la prochaine conférence inoubliable et tous les participants pour leur réseautage unique et leur bonne humeur.

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


All Articles