HolyJS 2019: Informe de SEMrush (Parte 2)



Esta es la segunda parte del análisis de tareas desde nuestro stand en la conferencia HolyJS , celebrada en San Petersburgo del 24 al 25 de mayo. Para un contexto más amplio, se recomienda que primero lea la primera parte de este material. Y si Countdown Expression ya se ha completado, bienvenido al siguiente paso.

A diferencia del oscurantismo en la primera tarea, las dos siguientes ya tienen algún indicio de la aplicabilidad de las aplicaciones normales en la vida. JavaScript todavía se está desarrollando bastante rápido y las soluciones a los problemas sugeridos resaltan algunas de las nuevas características del lenguaje.

Tarea 2 ~ Hecho por unos


Se suponía que el código se ejecutaría e imprimiría respuestas a la consola en respuesta a tres solicitudes, y luego "hecho". Pero algo salió mal ... Corrija la situación.

;(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'); })(); 

Problema de investigación


Que tenemos aqui Este es un objeto iterativo iterativo que tiene un símbolo Symbol.iterator bien conocido definido a través de una función generadora . Se declara una matriz fs en el cuerpo de la función, cuyos elementos caen en la función de búsqueda a su vez para enviar una solicitud y el resultado de cada llamada de función se devuelve a través de rendimiento . ¿Qué solicitudes envía la función de búsqueda ? Todos los elementos de la matriz fs son rutas relativas a los recursos con los números 1, 2 y 3, respectivamente. Por lo tanto, la URL completa se obtendrá concatenando location.origin con el siguiente número, por ejemplo:

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

A continuación, queremos iterar el objeto iter a través de for-of , para ejecutar cada solicitud a su vez con la salida del resultado, después de todo, imprima "hecho". ¡Pero eso no funciona! El problema es que buscar es una cosa asincrónica y devuelve una promesa, no una respuesta. Por lo tanto, en la consola veremos algo como esto:

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

En realidad, la tarea se reduce a resolver estas mismas promesas.

Tenemos async / aguardando


El primer pensamiento puede ser jugar con Promise.all : darle nuestro objeto iterable , luego la salida a la consola está "hecha". Pero no nos proporcionará la ejecución secuencial de las solicitudes (según lo requiera la condición), sino que simplemente las enviará todas y esperará la última respuesta antes de la resolución general.

La solución más simple aquí sería esperar en el cuerpo del cuerpo para esperar la resolución de la próxima promesa antes de enviarla a la consola:

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

Para que el trabajo en espera y "hecho" se muestre al final, debe hacer que la función principal sea asíncrona a través de asíncrono :

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

En este caso, el problema ya se ha resuelto (casi):

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

Iterador asíncrono y generador


Dejaremos la función principal asincrónica, pero para esperar hay un lugar más elegante en esta tarea que en el cuerpo for-of : este es el uso de la iteración asincrónica a través de for-wait-of , a saber:

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

¡Todo funcionará! Pero si recurre a la descripción de esta propuesta sobre iteración asincrónica, entonces esto es lo interesante:

Introducimos una variación de la declaración de iteración for-of que itera sobre objetos iterables asíncronos. Las declaraciones for-of asincrónicas solo se permiten dentro de las funciones asincrónicas y las funciones generadoras asincrónicas

Es decir, nuestro objeto no solo debe ser iterable , sino "asyncIterable" a través del nuevo símbolo conocido Symbol.asyncIterator y, en nuestro caso, ya una función generadora asíncrona:

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

¿Cómo funciona entonces en un iterador y generador regular? Sí, solo implícitamente, como mucho más en este idioma. Esto es difícil: si el objeto solo es iterable , entonces cuando itera asincrónicamente, "convierte" el objeto en asyncIterable envolviendo los elementos (si es necesario) en Promise con la expectativa de resolución. Habló con más detalle en un artículo de Axel Rauschmayer .

Probablemente, a través de Symbol.asyncIterator todavía será más correcto, ya que creamos explícitamente el objeto asyncIterable para nuestra iteración asincrónica a través de for-wait-of , mientras dejamos la oportunidad de complementar el objeto con un iterador regular para for , si es necesario. Si desea leer algo útil y suficiente en un artículo sobre iteraciones asíncronas en JavaScript, ¡ aquí está !

For -of asíncrono todavía está parcialmente en borrador, pero ya es compatible con los navegadores modernos (excepto Edge) y Node.js de 10.x. Si esto molesta a alguien, siempre puede escribir su propio pequeño polifilo para una cadena de promesas, por ejemplo, para un objeto iterable :

 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 esta y otras formas, descubrimos el envío de solicitudes y el procesamiento de respuestas a su vez. Pero en este problema hay un problema más pequeño pero molesto ...

Prueba de atención plena


Estábamos tan impresionados por todo este asincronismo que, como sucede a menudo, perdimos de vista un pequeño detalle. ¿Son esas solicitudes enviadas por nuestro script? Veamos la red :

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

Pero nuestros números son 1, 2, 3. Como si hubiera ocurrido una disminución. Por qué Solo en el código fuente de la tarea hay otro problema con la iteración, aquí:

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

Aquí se usa un for-in , que en lugar de los valores de la matriz omite sus propiedades enumeradas: y estos son los índices de elementos del 0 al 2. La función fetch todavía los lleva a cadenas y, a pesar de la ausencia de una barra inclinada antes (esto ya no es una ruta ), se resuelve relativamente URL de la página actual. Arreglarlo es mucho más fácil que darse cuenta. Dos opciones:

 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]); } 

En el primero, utilizamos el mismo for-of para iterar sobre los valores de la matriz, en el segundo: acceso al elemento de la matriz por índice.

Motivación


Consideramos 3 soluciones: 1) a través de esperar en el cuerpo for-for , 2) a través de for-wait-of, y 3) a través de nuestro polyfile (función recursiva, tubería , etc.). Es curioso que estas opciones dividieron a los participantes de la conferencia aproximadamente por igual y no se reveló ningún favorito obvio. En proyectos grandes, para tareas tan reales, generalmente se usa una biblioteca reactiva (por ejemplo, RxJS ), pero vale la pena recordar las características nativas modernas de un lenguaje de naturaleza asincrónica.

Aproximadamente la mitad de los participantes no notaron errores en la iteración de la lista de recursos, lo cual también es una observación interesante. Centrándonos en un problema no trivial pero obvio, podemos omitir fácilmente este truco aparentemente, pero con posibles consecuencias graves.

Problema 3 ~ Factor XIX


¡Cuántas veces en el registro del número 2019! (factorial de 2019) ¿aparece el número 19? Junto con la respuesta, proporcione una solución de JavaScript.

Problema de investigación


El problema está en la superficie: necesitamos un registro de un número muy grande para encontrar en él el número de todas las ocurrencias de la subcadena "19". Al resolver el problema en los números , nos encontramos rápidamente con Infinity (después de 170) y no obtuvimos nada. Además, el formato para representar números float64 garantiza la precisión de solo 15-17 caracteres, y necesitamos no solo un registro completo, sino también exacto del número. Por lo tanto, la dificultad principal es determinar la estructura para la acumulación de este gran número.

Grandes enteros


Si sigue las innovaciones del lenguaje, el problema se resuelve simplemente: en lugar del número de tipo , puede usar el nuevo tipo BigInt (etapa 3) , que le permite trabajar con números de precisión arbitrarios. Con la función recursiva clásica para calcular factorial y encontrar coincidencias a través de String.prototype.split, la primera solución se ve así:

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

Sin embargo, dos mil llamadas de función en la pila ya pueden ser peligrosas . Incluso si lleva la solución a la recursividad de cola , la optimización de llamadas de cola solo es compatible con Safari. El problema factorial aquí es más agradable de resolver a través de un ciclo aritmético o Array.prototype.reduce :

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

Esto puede parecer un procedimiento increíblemente largo. Pero esta impresión es engañosa. Si estima, entonces solo necesitamos gastar un poco más de dos mil multiplicaciones. En i5-4590 3.30GHz en cromo, el problema se resuelve en promedio en 4-5ms (!).

Otra opción para encontrar coincidencias en una cadena con el resultado de un cálculo es String.prototype.match por expresión regular con el indicador de búsqueda global: / 19 / g .

Aritmética grande


Pero, ¿qué pasa si todavía no tenemos este BigInt (y las bibliotecas también)? En este caso, puede hacer la aritmética larga usted mismo. Para resolver el problema, nos basta con implementar solo la función de multiplicar grande por pequeña (multiplicamos por números del 1 al 2019). Podemos mantener un gran número y el resultado de la multiplicación, por ejemplo, en la línea:

 /** * @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 

Aquí simplemente multiplicamos la columna en bits desde el final de la línea hasta el principio, como nos enseñaron en la escuela. Pero la solución ya requiere unos 170 ms.

Podemos mejorar algo el algoritmo procesando más de un dígito en un registro de números a la vez. Para hacer esto, modificamos la función y al mismo tiempo vamos a las matrices, para no perder el tiempo con las líneas cada vez:

 /** * @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(); } 

Aquí, los números grandes están representados por una matriz, cada elemento del cual almacena información sobre dígitos dígitos del registro de números, usando el número . Por ejemplo, el número 2016201720182019 con dígitos = 3 se representará como:

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

Al convertir a una línea antes de una unión, debe recordar los ceros iniciales. La función factor devuelve el factorial calculado por una cadena, utilizando la función múltiple con el número especificado de dígitos procesados ​​a la vez en la representación "masiva" del número al calcular:

 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 

La implementación "hasta la rodilla" a través de matrices resultó ser más rápida que a través de cadenas, y con dígitos = 1 calcula la respuesta ya en promedio en 90ms, dígitos = 3 en 35ms, dígitos = 6 en solo 20ms. Sin embargo, recuerde que al aumentar el número de dígitos, nos estamos acercando a una situación en la que multiplicar el número por el número "debajo del capó" puede estar fuera de la caja fuerte MAX_SAFE_INTEGER . Puedes jugar con eso aquí . ¿Cuál es el valor máximo de dígitos que podemos permitirnos para esta tarea?

Los resultados ya son bastante indicativos, BigInt es realmente bastante rápido:



Motivación


Es genial que 2/3 de los participantes de la conferencia usaron el nuevo tipo BigInt en la solución (alguien admitió que esta fue la primera experiencia). El tercio restante de las soluciones contenía sus propias implementaciones de aritmética larga en cadenas o matrices. La mayoría de las funciones implementadas multiplicaron números grandes por números grandes, cuando para una solución fue suficiente multiplicar por un número "pequeño" y pasar un poco menos de tiempo. Bien, ¿ya usas BigInt en tus proyectos?

Agradecimientos


Estos dos días de la conferencia estuvieron muy llenos de discusiones y aprendiendo algo nuevo. Me gustaría agradecer al comité del programa por la próxima conferencia inolvidable y a todos los participantes por su trabajo en red único y su buen humor.

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


All Articles