No sé sobre ti, pero para mí no hay mejor comienzo para el día que preocuparse por la programación. La sangre hierve al ver una crítica exitosa de uno de los lenguajes "audaces" utilizados por los plebeyos, atormentado durante todo el día de trabajo entre visitas tímidas a StackOverflow.
(Mientras tanto, usted y yo usamos solo el lenguaje más ilustrado y las herramientas sofisticadas diseñadas para las manos hábiles de maestros como nosotros).
Por supuesto, como autor del sermón, tomo riesgos. ¡Te puede gustar el idioma del que me burlo! Un folleto imprudente podría haber traído inadvertidamente a mi blog una furiosa multitud de móviles con horquillas y antorchas listas.
Para protegerme del fuego justo y no ofender tus sentimientos (probablemente delicados), hablaré sobre el lenguaje ...
... quien acaba de llegar. Sobre una efigie de paja, cuyo único papel es quemar a los críticos en la hoguera.
Sé que esto suena tonto, pero créanme, al final veremos qué cara (o caras) fueron pintadas en una cabeza de paja.
Nuevo idioma
Será una inflexión aprender un idioma completamente nuevo (y desagradable) solo para un artículo de blog, así que digamos que es muy similar al idioma que ya conocemos. Por ejemplo Javascript. Tirantes y puntos y comas. if
, while
, etc. - Lingua franca de nuestra multitud.
Elegí JS no porque este artículo sea sobre él. Es solo un idioma en el que el lector promedio probablemente entrará. Voila
function thisIsAFunction(){ return "!"; }
Dado que nuestro animal de peluche es un lenguaje genial (lectura - mala), tiene funciones de primera clase . Entonces puedes escribir algo como esto:
Esta es una de las características de primera clase , y como su nombre lo indica, son geniales y súper útiles. Probablemente estés acostumbrado a transformar las colecciones de datos con su ayuda, pero tan pronto como captas el concepto, comienzas a usarlo en todas partes, maldita sea.
Quizás en pruebas:
describe("", function(){ it(" ", function(){ expect("").not.toBe(""); }); };
O cuando necesita analizar (analizar) los datos:
tokens.match(Token.LEFT_BRACKET, function(token){
Luego, después de acelerar, escribes todo tipo de geniales bibliotecas y aplicaciones reutilizables que giran en torno a funciones, llamadas a funciones, retornos de funciones desde funciones, una cabina funcional.
traductor: en el original "Functapalooza". El prefijo -a-palooza es tan genial que quieres compartirlo con todos.
¿De qué color es tu función?
Y aquí comienzan las rarezas. Nuestro lenguaje tiene una característica peculiar:
1. Cada función tiene un color.
Cada función, una devolución de llamada anónima o una función regular con un nombre, es roja o azul. Dado que el resaltado del código en nuestro blog no resalta el color diferente de las funciones, aceptemos que la sintaxis es:
blue*function doSomethingAzure(){
Nuestro lenguaje no tiene funciones incoloras. ¿Quieres hacer una función? - Debe elegir un color. Estas son las reglas. Y hay algunas reglas más que debes seguir:
2. El color afecta la forma en que se llama la función
Imagine que hay dos sintaxis para llamar a funciones: "azul" y "rojo". Algo como:
doSomethingAzure(...)*blue; doSomethingCarnelian()*red;
Cuando llama a una función, debe usar una llamada que coincida con su color. Si no lo ha adivinado, llamaron a la función roja con *blue
después de los corchetes (o viceversa), sucederá algo muy malo. Una pesadilla infantil olvidada hace mucho tiempo, como un payaso con serpientes en lugar de manos que se escondía debajo de tu cama. Saltará del monitor y te chupará los ojos.
Estúpida regla, ¿verdad? Oh, pero una cosa más:
3. Solo la función roja puede causar la función roja.
Puede llamar a la función azul desde rojo. Esto es kosher:
red*function doSomethingCarnelian(){ doSomethingAzure()*blue; }
Pero no al revés. Si lo intentas:
blue*function doSomethingAzure(){ doSomethingCarnelian()*red; }
- Serás visitado por el viejo Clown Spider Maw.
Esto hace que sea más difícil escribir funciones más altas, como filter()
del ejemplo. Debemos elegir un color para cada nueva función y esto afecta el color de las funciones que podemos pasarle. La solución obvia es hacer que filter()
rojo. Entonces podemos llamar al menos funciones rojas, al menos funciones azules. Pero luego nos lastimamos por la próxima espina en la corona de espinas, que es el lenguaje dado:
4. Las funciones rojas causan dolor
No identificaremos este "dolor", solo imagine que el programador debe saltar a través del aro cada vez que llama a la función roja. La llamada puede ser demasiado polisilábica o no puede ejecutar la función dentro de algunas expresiones. O solo puede acceder a la función roja desde líneas impares.
No importa lo que sea, pero si decides hacer que la función sea roja, todos los que usen tu API querrán escupir en tu café o hacer algo peor.
La solución obvia en este caso es nunca usar funciones rojas. Simplemente haga que todo sea azul, y estará de vuelta en el mundo normal, donde todas las funciones son del mismo color, lo que equivale al hecho de que no tienen color y que nuestro lenguaje no es completamente tonto.
Por desgracia, los sádicos que desarrollaron este lenguaje (todos saben que los autores de los lenguajes de programación son sádicos, ¿verdad?) Metan la última espina:
5. Algunas de las funciones centrales del lenguaje son rojas.
Algunas funciones integradas en la plataforma, funciones que necesitamos usar que no pueden ser escritas por nosotros mismos, están disponibles solo en rojo. En este punto, una persona inteligente puede comenzar a sospechar que este lenguaje nos odia.
¡Todo esto es culpa de los lenguajes funcionales!
Puede pensar que el problema es que estamos tratando de usar funciones de orden superior. Si simplemente dejamos de perder el tiempo con todas estas tonterías funcionales, y comenzamos a escribir funciones azules normales de primer orden (funciones que no funcionan con otras funciones, aprox. Traductor), según lo planeado por Dios, nos libraremos de todo este dolor.
Si llamamos solo funciones azules, hacemos todas nuestras funciones azules. De lo contrario, hacemos rojo. Hasta que creamos funciones que acepten funciones, no debemos preocuparnos por el "polimorfismo del color de la función" (¿policromático?) U otras tonterías.
Pero, por desgracia, las funciones de orden superior son solo un ejemplo. El problema surge cada vez que queremos dividir nuestro programa en funciones para su reutilización.
Por ejemplo, tenemos un pequeño código que, bueno, no sé, implementa el algoritmo de Dijkstra sobre un gráfico que representa cuánto se presionan mutuamente sus conexiones sociales. (Pasé mucho tiempo tratando de decidir qué significaría el resultado. ¿Deseos transitivos?)
Más tarde, necesitaba usar este algoritmo en otro lugar. Naturalmente, envuelve el código en una función separada. Llámala del lugar viejo y del nuevo. ¿Pero de qué color debe ser la función? Probablemente intente hacerlo azul, pero ¿qué pasa si usa una de estas desagradables funciones "solo rojas" de la biblioteca del kernel?
Digamos que el nuevo lugar desde el que desea llamar a la función es azul. Pero ahora necesita reescribir el código de llamada en rojo. Y luego rehaga la función que llama a este código. Uf Tendrás que recordar constantemente el color de todos modos. Esta será la arena en sus bañadores en una programación de vacaciones en la playa.
Alegoría de color
De hecho, no estoy hablando de color. Esta es una alegoría, un recurso literario. Joder , no se trata de las estrellas en las barrigas , se trata de la carrera. Probablemente ya sospeches ...
Funciones rojas: asincrónicas
Si programa en JavaScript o Node.js, cada vez que define una función que llama a una función de devolución de llamada (devolución de llamada) para "devolver" el resultado, escribe una función roja. Mira esta lista de reglas y observa cómo encajan en mi metáfora:
- Las funciones síncronas devuelven un resultado, las funciones asíncronas no; a cambio, llaman a una devolución de llamada.
- Las funciones síncronas devuelven el resultado como un valor de retorno, las funciones asíncronas lo devuelven, provocando la devolución de llamada que les pasó.
- No puede llamar a una función asincrónica desde una síncrona, porque no puede conocer el resultado hasta que la función asincrónica se ejecute más tarde.
- Las funciones asincrónicas no se compilan en expresiones debido a devoluciones de llamada, requieren que sus errores se manejen de manera diferente y no se pueden usar en un bloque
try/catch
o en una serie de otras expresiones que controlan el programa. - Todo lo relacionado con Node.js es que la biblioteca del kernel es asíncrona. (Aunque vuelven a donar y comienzan a agregar versiones de
_Sync()
a muchas cosas).
Cuando las personas hablan del "infierno de devolución de llamada" , hablan de lo molesto que es tener funciones "rojas" en su idioma. Cuando crean 4089 bibliotecas para programación asincrónica (en 2019 ya 11217 - aprox. Traductor), intentan hacer frente al problema a nivel de biblioteca de que se han quedado atrapados con el lenguaje.
Prometo que el futuro es mejor
en la traducción: "Prometo que el futuro es mejor" se pierde el juego de palabras del título y el contenido de la sección
La gente de Node.js hace tiempo que se dio cuenta de que las devoluciones de llamada duelen y estaban buscando soluciones. Una de las técnicas que ha inspirado a muchas personas son las promises
, que también puede conocer por el futures
apodo.
en la informática rusa, en lugar de traducir "promesas" como "promesas", se estableció un documento de seguimiento del inglés: "promesas". La palabra "Futuros" se usa tal cual, probablemente porque los "futuros" ya están ocupados por la jerga financiera.
Promis es un contenedor para la devolución de llamadas y el controlador de errores. Si está pensando en pasar una devolución de llamada para el resultado y otra devolución de llamada para el error, entonces el future
es la encarnación de esta idea. Este es un objeto básico que es una operación asincrónica.
Acabo de recibir un montón de palabras elegantes y puede sonar como una gran solución, pero principalmente es aceite de serpiente . Las promesas realmente hacen que escribir código asincrónico sea un poco más fácil. Son más fáciles de componer en expresiones, por lo que la regla 4 es un poco menos estricta.
Pero para ser honesto, es como la diferencia entre un golpe en el estómago o la ingle. Sí, no duele tanto, pero nadie estará encantado con esa elección.
Aún no puede usar promesas con manejo de excepciones u otros
operadores de gestión. No puede llamar a una función que devuelve el future
del código síncrono. ( puede , pero el próximo responsable del mantenimiento de su código inventará una máquina del tiempo, regresará en el momento en que lo hizo y se pegará un lápiz en la cara por la razón # 2).
Las promesas aún dividen su mundo en mitades asíncronas y sincrónicas con todo el sufrimiento resultante. Entonces, incluso si su idioma admite promises
o futures
, todavía se parece mucho al mio.
(Sí, esto incluso incluye el Dart que uso. Por lo tanto, estoy muy contento de que parte del equipo esté intentando otros enfoques para el paralelismo )
enlace del proyecto abandonado oficialmente
Estoy esperando una solución
Los programadores de C # probablemente se sienten complacientes (la razón por la que se están convirtiendo en más y más víctimas es que Halesberg y la compañía rocían todo y rocían el lenguaje con azúcar sintáctico). En C #, puede usar la palabra clave await
para llamar a una función asincrónica.
Esto hace que hacer llamadas asincrónicas sea tan fácil como sincrónico, con la adición de una pequeña y linda palabra clave. Puede insertar una llamada en await
en las expresiones, utilizarlas en el manejo de excepciones, en el flujo de instrucciones. Puedes volverte loco. Deja que la lluvia espere como dólares para tu nuevo álbum de rapero.
Async-await es bueno, así que lo agregamos a Dart. Es mucho más fácil escribir código asincrónico con él. Pero, como siempre, hay un "Pero". Aqui esta Pero ... aún divides el mundo por la mitad. Las funciones asincrónicas ahora son más fáciles de escribir, pero siguen siendo funciones asincrónicas.
Aún tienes dos colores. Async-await resuelve el molesto problema # 4: hacen que llamar a funciones rojas no sea más difícil que llamar a funciones azules. Pero el resto de las reglas todavía están aquí:
- Las funciones síncronas devuelven valores, las funciones asíncronas devuelven un contenedor (
Task<T>
en C # o Future<T>
en Dart) alrededor del valor. - Sincrónico acaba de llamar, asíncrono necesita
await
. - Al llamar a una función asincrónica, obtienes un objeto contenedor cuando realmente quieres un valor. No puede expandir el valor hasta que haga que su función sea asíncrona y la llame con
await
(pero vea el siguiente párrafo). - Además de esperar un poco la decoración, al menos resolvimos este problema.
- La biblioteca central de C # es más antigua que la asincronía, por lo que creo que nunca tuvieron este problema.
Async
realmente mejor. Preferiría async-wait a devoluciones de llamadas desnudas cualquier día de la semana. Pero nos mentimos a nosotros mismos si creemos que todos los problemas están resueltos. Tan pronto como comience a escribir funciones de orden superior, o reutilice el código, nuevamente se dará cuenta de que el color todavía está allí, desangrando todo su código fuente.
¿Qué idioma no es el color?
Entonces JS, Dart, C # y Python tienen este problema. CoffeeScript y la mayoría de los otros lenguajes que compilan en JS también (y Dart heredó). Creo que incluso ClojureScript tiene esta trampa, a pesar de sus esfuerzos activos con core.async
¿Quieres saber cuál no? Java Estoy en lo cierto ¿Con qué frecuencia dice: "Sí, Java solo lo está haciendo bien"? Y así sucedió. En su defensa, están tratando activamente de corregir su supervisión promoviendo futures
y async IO. Es como una carrera peor que peor.
todo ya esta en Java
C #, de hecho, también puede solucionar este problema. Eligieron tener color. Antes de agregar async-await y toda esta Task<T>
basura, podría usar llamadas API síncronas regulares. Otros tres idiomas que no tienen un problema de "color": Go, Lua y Ruby.
Adivina lo que tienen en común?
Corrientes O más precisamente: muchas pilas de llamadas independientes que pueden cambiar . Estos no son necesariamente hilos del sistema operativo. Las corutinas en Go, las corutinas en Lua y los hilos en Ruby son todos adecuados.
(Es por eso que existe esta pequeña advertencia para C #: puede evitar el dolor asincrónico en C # utilizando hilos).
Memoria de operaciones pasadas
El problema fundamental es "¿cómo continuar desde el mismo lugar cuando se completa la operación (asíncrona)"? Te sumergiste en el abismo de la pila de llamadas y luego llamaste a algún tipo de operación de E / S. En aras de la aceleración, esta operación utiliza la API asincrónica subyacente de su sistema operativo. No puedes esperar a que se complete. Debe volver al bucle de eventos de su idioma y darle tiempo al sistema operativo para completar la operación.
Una vez que esto sucede, debe reanudar lo que estaba haciendo. Por lo general, el lenguaje "recuerda dónde estaba" a través de la pila de llamadas . Sigue todas las funciones que se han llamado en este momento y observa dónde se muestra el contador de comandos en cada una de ellas.
Pero para realizar E / S asíncronas, debe relajarse, descartar toda la pila de llamadas en C. Escriba Trick-22. ¡Tiene E / S súper rápidas, pero no puede usar el resultado! Todos los idiomas con E / S asíncronas bajo el capó, o, en el caso de JS, el bucle de eventos del navegador, se ven obligados a manejar esto de alguna manera.
Node, con sus devoluciones de llamadas que marchan para siempre, cierra todas estas llamadas. Cuando escribes:
function makeSundae(callback) { scoopIceCream(function (iceCream) { warmUpCaramel(function (caramel) { callback(pourOnIceCream(iceCream, caramel)); }); }); }
Cada una de estas expresiones funcionales cierra todo su contexto circundante. Esto transfiere parámetros, como iceCream
y caramel
, desde la pila de llamadas al montón . Cuando una función externa devuelve un resultado y la pila de llamadas se destruye, es genial. Los datos todavía están en algún lugar del montón.
El problema es que tienes que resucitar cada una de estas malditas llamadas nuevamente. Incluso hay un nombre especial para esta conversión: estilo de paso continuo
enlace feroz funcionalidad
Esto fue inventado por los piratas informáticos en los años 70, como una representación intermedia para su uso bajo el capó de los compiladores. Esta es una forma muy extraña de introducir código que facilita la realización de algunas optimizaciones del compilador.
Nadie pensó que un programador podría escribir ese código . Y luego apareció Node, y de repente todos pretendemos escribir un backend compilador. ¿Dónde nos volteamos por el camino equivocado?
Tenga en cuenta que las promesas y los futures
realmente no ayudan mucho. Si los usa, sabe que todavía está acumulando capas gigantes de expresiones funcionales . Simplemente los pasa a .then()
lugar de a la función asincrónica en sí.
En espera de una solución generada
Async-waitit realmente ayuda. Si mira debajo del capó al compilador cuando se encuentra await
, verá que realmente realiza la conversión de CPS. Es por eso que debe usar await
en C # - esto es una pista para el compilador - "detenga la función en el medio aquí". Todo después de await
convierte en una nueva función que el compilador sintetiza en su nombre.
Es por eso que async-await no necesita soporte de tiempo de ejecución dentro del marco .NET. El compilador compila esto en una cadena de cierres relacionados, que ya sabe cómo manejar. (Curiosamente, los cierres tampoco requieren soporte en tiempo de ejecución. Se compilan en clases anónimas. En C #, los cierres son solo objetos).
Probablemente te estés preguntando cuando menciono los generadores. ¿Hay yield
en tu idioma? Entonces él puede hacer algo muy similar.
(Creo que los generadores y la espera asíncrona son en realidad isomórficos. En algún lugar de los rincones polvorientos y grietas de mi disco duro se encuentra una pieza de código que implementa el bucle del juego en los generadores usando solo la espera asíncrona).
Entonces, ¿dónde estoy? Oh si Entonces, con devoluciones de llamada, promesas, espera asíncrona y generadores, terminas tomando tu función asincrónica y dividiéndola en un montón de cierres que viven en el montón.
Su función llama externa en tiempo de ejecución. Cuando finaliza el bucle de eventos o la operación de E / S, se llama a su función y continúa desde donde estaba. Pero esto significa que todo lo que esté por encima de su función también debería volver. Aún necesita restaurar toda la pila.
Aquí es de donde viene la regla: "Puedes llamar a la función roja solo desde la función roja". Debe guardar toda la pila de llamadas en cierres en main()
o en el controlador de eventos.
Implementación de pila de llamadas
Pero usando hilos ( verde o nivel de sistema operativo), no necesita hacer esto. Simplemente puede pausar todo el hilo y saltar al sistema operativo o al bucle de eventos sin tener que regresar de todas estas funciones .
El lenguaje Go, en mi opinión, lo hace a la perfección. Tan pronto como realice alguna operación de E / S, Go estacionará esta rutina y continuará cualquier otra que no esté bloqueada por E / S.
Si observa las operaciones de E / S en la biblioteca estándar de Golang, parecen sincronizadas. En otras palabras, simplemente funcionan y luego devuelven el resultado cuando están listos. Pero esta sincronización no significa lo mismo que en Javascript. Go- , IO . Go .
Go — , . , , .
, API, , . .
, , . , . , 50% .
, , , .
Javascript -, , , JS , JS , . , JS .
, ( ) — , , , async
. import threading
( , AsyncIO, Twisted Tornado, ).
, , , , , , .
, Go, Go .
, , , ( - ) , "async-await ". .
, .
, , .