Comentario del traductor: Esta es una traducción de un gran artículo de Dan Abramov, un colaborador de React. Sus ejemplos están escritos para JS, pero serán igualmente claros para los desarrolladores en cualquier idioma. La idea es común a todos.
¿Has oído hablar de los efectos algebraicos?
Mis primeros intentos de descubrir quiénes son y por qué deberían excitarme no tuvieron éxito. Encontré varios archivos PDF , pero me confundieron aún más. (Por alguna razón, me quedo dormido mientras leo artículos académicos).
Pero mi colega Sebastian continuó llamándolos el modelo mental de algunas de las cosas que hacemos en React. (Sebastian trabaja en el equipo React y propuso muchas ideas, incluyendo Hooks y Suspense.) En algún momento, se convirtió en un meme local en el equipo React, y muchas de nuestras conversaciones terminaron con lo siguiente:
Resultó que los efectos algebraicos son un concepto genial, y no es tan aterrador como me pareció al principio después de leer estos archivos PDF. Si solo usa React, no necesita saber nada sobre ellos, pero si usted, como yo, está interesado, siga leyendo.
(Descargo de responsabilidad: no soy un investigador en el campo de los lenguajes de programación y puede haber estropeado algo en mi explicación. ¡Entonces avíseme si me equivoco!)
Todavía es temprano en la producción
Los efectos algebraicos son actualmente un concepto experimental del campo de estudio de los lenguajes de programación. Esto significa que, a diferencia de if
, for
o incluso expresiones async/await
, lo más probable es que no pueda usarlas en este momento en producción. Solo son compatibles con unos pocos idiomas que se crearon específicamente para estudiar esta idea. Hay avances en su implementación en OCaml, que ... todavía está en curso . En otras palabras, mire, pero no toque con las manos.
¿Por qué debería molestarme?
Imagine que está escribiendo código usando goto
, y alguien le está contando sobre la existencia de construcciones if
y for
. O tal vez estás atrapado en un infierno de devolución de llamada y alguien te muestra async/await
. Muy bien, ¿no?
Si usted es el tipo de persona a la que le gusta aprender innovaciones de programación unos años antes de que se ponga de moda, podría ser el momento de interesarse en los efectos algebraicos. Aunque no es necesario. Así es como se habla de async/await
en 1999.
Bueno, ¿qué tipo de efectos son estos?
El nombre puede ser un poco confuso, pero la idea es simple. Si está familiarizado con try/catch
bloques try/catch
, comprenderá rápidamente los efectos algebraicos.
Recordemos try/catch
. Digamos que tiene una función que arroja excepciones. Quizás haya varias llamadas anidadas entre este y el catch
:
function getName(user) { let name = user.name; if (name === null) { throw new Error(' '); } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } catch (err) { console.log(", : ", err); }
Lanzamos una excepción dentro de getName
, pero "aparece" a través de makeFriends
hasta el catch
más cercano. Esta es la propiedad principal de try/catch
. El código intermedio no es necesario para preocuparse por el manejo de errores.
A diferencia de los códigos de error en lenguajes como C, al usar try/catch
no tiene que pasar los errores manualmente a través de cada nivel intermedio para manejar el error en el nivel superior. Las excepciones aparecen automáticamente.
¿Qué tiene esto que ver con los efectos algebraicos?
En el ejemplo anterior, tan pronto como veamos un error, no podremos continuar ejecutando el programa. Cuando nos encontramos en un catch
, la ejecución normal del programa se detendrá.
Se acabó todo. Es muy tarde Lo mejor que podemos hacer es recuperarnos del fracaso y quizás de alguna manera repetir lo que estábamos haciendo, pero no podemos mágicamente "regresar" a donde estábamos y hacer otra cosa. Y con efectos algebraicos, podemos.
Este es un ejemplo escrito en un hipotético dialecto de JavaScript (llamémoslo ES2025 por diversión), que nos permite seguir trabajando después del user.name
faltante.
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
(Pido disculpas a todos los lectores de 2025 que buscan "ES2025" en Internet y entran en este artículo. Si para entonces los efectos algebraicos se convirtieran en parte de JavaScript, ¡me complacería actualizar el artículo!)
En lugar de throw
usamos la palabra clave hipotética perform
. Del mismo modo, en lugar de try/catch
usamos el hipotético try/handle
. La sintaxis exacta no importa aquí : se me ocurrió algo para ilustrar la idea.
Entonces, ¿qué está pasando aquí? Echemos un vistazo más de cerca.
En lugar de lanzar un error, llevamos a cabo el efecto . Así como podemos lanzar cualquier objeto, aquí podemos pasar algún valor para el procesamiento . En este ejemplo, paso una cadena, pero puede ser un objeto o cualquier otro tipo de datos:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; }
Cuando lanzamos una excepción, el motor busca el controlador try/catch
más cercano en la pila de llamadas. De manera similar, cuando ejecutamos un efecto , el motor buscará el try/handle
efecto de try/handle
más cercano en la parte superior de la pila:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Este efecto nos permite decidir cómo manejar la situación cuando no se especifica el nombre. Nuevo aquí (en comparación con las excepciones) es el resume with
hipotético resume with
:
try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
Esto es algo que no puedes hacer con try/catch
. Nos permite volver a donde realizamos el efecto y pasar algo del controlador . : -O
function getName(user) { let name = user.name; if (name === null) {
Se necesita un poco de tiempo para sentirse cómodo, pero conceptualmente no es muy diferente de try/catch
con un retorno.
Sin embargo, tenga en cuenta que los efectos algebraicos son una herramienta mucho más poderosa que simplemente try/catch
. La recuperación de errores es solo uno de los muchos casos de uso posibles. Comencé con este ejemplo solo porque era más fácil de entender.
La función no tiene color.
Los efectos algebraicos tienen implicaciones interesantes para el código asincrónico.
En idiomas con async/await
funciones suelen tener un "color" ( ruso ). Por ejemplo, en JavaScript, no podemos hacer que getName
asíncrono sin infectar a makeFriends
y sus funciones de llamada con async. Esto puede ser un verdadero problema si parte del código a veces necesita ser síncrono y a veces asíncrono.
Los generadores de JavaScript funcionan de manera similar : si trabaja con generadores, entonces todo el código intermedio también debe saber acerca de los generadores.
Bueno, ¿qué tiene que ver con eso?
Por un momento, olvidémonos de async / wait y regresemos a nuestro ejemplo:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { resume with ' '; } }
¿Qué pasa si nuestro controlador de efectos no puede devolver el "nombre de repuesto" de forma síncrona? ¿Qué pasa si queremos obtenerlo de la base de datos?
Resulta que podemos llamar a resume with
asincrónicamente desde nuestro controlador de efectos sin hacer ningún cambio en getName
o makeFriends
:
function getName(user) { let name = user.name; if (name === null) { name = perform 'ask_name'; } return name; } function makeFriends(user1, user2) { user1.friendNames.add(getName(user2)); user2.friendNames.add(getName(user1)); } const arya = { name: null }; const gendry = { name: '' }; try { makeFriends(arya, gendry); } handle(effect) { if (effect === 'ask_name') { setTimeout(() => { resume with ' '; }, 1000); } }
En este ejemplo, llamamos a resume with
solo un segundo después. Puede considerar resume with
devolución de llamada, a la que puede llamar solo una vez. (También puede presumir ante sus amigos llamando a esto "una continuación limitada de una sola vez" (el término continuación delimitada aún no ha recibido una traducción estable al ruso - aprox. Transl.)).
Ahora la mecánica de los efectos algebraicos debería ser un poco más clara. Cuando arrojamos un error, el motor de JavaScript hace girar la pila al destruir las variables locales en el proceso. Sin embargo, cuando ejecutamos el efecto, nuestro motor hipotético crea una devolución de llamada (en realidad un "marco de continuación", aprox. Transl.) Con el resto de nuestra función, y la resume with
lo llamará.
Una vez más, un recordatorio: la sintaxis específica y las palabras clave específicas se inventan completamente solo para este artículo. El punto no está en eso, sino en la mecánica.
Nota de limpieza
Vale la pena señalar que los efectos algebraicos surgieron como resultado del estudio de la programación funcional. Algunos de los problemas que resuelven son exclusivos de la programación funcional. Por ejemplo, en lenguajes que no permiten efectos secundarios arbitrarios (como Haskell), debe usar conceptos como mónadas para arrastrar los efectos a través de su programa. Si alguna vez has leído el tutorial de mónada, entonces sabes que puede ser difícil de entender. Los efectos algebraicos ayudan a hacer algo similar con un poco menos de esfuerzo.
Es por eso que la mayoría de las discusiones sobre los efectos algebraicos son completamente incomprensibles para mí. (No conozco a Haskell y sus "amigos"). Sin embargo, creo que incluso en un lenguaje inmundo como JavaScript, los efectos algebraicos pueden ser una herramienta muy poderosa para separar el "qué" del "cómo" en su código.
Le permiten escribir código que describe lo que está haciendo:
function enumerateFiles(dir) { const contents = perform OpenDirectory(dir); perform Log('Enumerating files in ', dir); for (let file of contents.files) { perform HandleFile(file); } perform Log('Enumerating subdirectories in ', dir); for (let directory of contents.dir) {
Y luego envuélvelo con algo que describa el "cómo" lo haces:
let files = []; try { enumerateFiles('C:\\'); } handle(effect) { if (effect instanceof Log) { myLoggingLibrary.log(effect.message); resume; } else if (effect instanceof OpenDirectory) { myFileSystemImpl.openDir(effect.dirName, (contents) => { resume with contents; }); } else if (effect instanceof HandleFile) { files.push(effect.fileName); resume; } }
Lo que significa que estas partes pueden convertirse en una biblioteca:
import { withMyLoggingLibrary } from 'my-log'; import { withMyFileSystem } from 'my-fs'; function ourProgram() { enumerateFiles('C:\\'); } withMyLoggingLibrary(() => { withMyFileSystem(() => { ourProgram(); }); });
A diferencia de async / await o generadores, los efectos algebraicos no requieren la complicación de funciones "intermedias". Nuestro llamado a enumerateFiles
puede estar en lo profundo de nuestro programa, pero siempre que haya un controlador de efectos para cada uno de los efectos que pueda ejecutar en algún lugar arriba, nuestro código continuará funcionando.
Los manejadores de efectos nos permiten separar la lógica del programa de implementaciones específicas de sus efectos sin necesidad de bailar y código de plantilla innecesarios. Por ejemplo, podríamos redefinir completamente el comportamiento en las pruebas para usar el sistema de archivos falso y hacer instantáneas de registros en lugar de mostrarlos en la consola:
import { withFakeFileSystem } from 'fake-fs'; function withLogSnapshot(fn) { let logs = []; try { fn(); } handle(effect) { if (effect instanceof Log) { logs.push(effect.message); resume; } }
Dado que las funciones no tienen un "color" (el código intermedio no tiene que saber acerca de los efectos), y los controladores de efectos se pueden componer (se pueden anidar), puede crear abstracciones muy expresivas con ellos.
Nota de tipos
Dado que los efectos algebraicos provienen de lenguajes tipados estáticamente, la mayor parte del debate sobre ellos se centra en cómo expresarlos en tipos. Sin duda, esto es importante, pero también puede complicar la comprensión del concepto. Es por eso que este artículo no habla sobre tipos en absoluto. Sin embargo, debo tener en cuenta que, por lo general, el hecho de que una función pueda realizar un efecto se codificará con una firma de su tipo. Por lo tanto, estará protegido de una situación en la que se realizan efectos impredecibles o no puede rastrear de dónde provienen.
Aquí puede decir que los efectos técnicamente algebraicos "dan color" a las funciones en lenguajes estáticamente tipados, ya que los efectos son parte de una firma de tipo. Realmente lo es Sin embargo, arreglar la anotación de tipo para que una función intermedia incluya un nuevo efecto no es en sí un cambio semántico, a diferencia de agregar asíncrono o convertir la función en un generador. La inferencia de tipos también puede ayudar a evitar la necesidad de cambios en cascada. Una diferencia importante es que puede "suprimir" los efectos insertando un trozo vacío o una implementación temporal (por ejemplo, una llamada de sincronización para un efecto asincrónico), que, si es necesario, le permite evitar su efecto en el código externo, o convertirlo en otro efecto.
¿Necesito efectos algebraicos en JavaScript?
Honestamente, no lo se. Son muy poderosos y se puede argumentar que son demasiado poderosos para un lenguaje como JavaScript.
Creo que podrían ser muy útiles para lenguajes donde la mutabilidad es rara y donde la biblioteca estándar admite completamente los efectos. Si primero realiza perform Timeout(1000), perform Fetch('http://google.com')
y perform ReadFile('file.txt')
, y su idioma tiene "coincidencia de patrones" y escritura estática para los efectos, luego Este puede ser un entorno de programación muy agradable.
¡Quizás este lenguaje incluso se compilará en JavaScript!
¿Qué tiene esto que ver con React?
No muy grande Incluso puedes decir que tiré una lechuza en un globo.
Si viste mi charla sobre Time Slicing and Suspense, entonces la segunda parte incluye componentes que leen datos del caché:
function MovieDetails({ id }) {
(El informe usa una API ligeramente diferente, pero ese no es el punto).
Este código se basa en la función Reaccionar para muestras de datos llamada " Suspense
", que se encuentra actualmente en desarrollo activo. Lo interesante aquí, por supuesto, es que los datos pueden no estar todavía en movieCache; en este caso, primero debemos hacer algo, porque no podemos continuar la ejecución. Técnicamente, en este caso, la llamada a read () arroja Promise (sí, arroja Promise - tienes que tragar este hecho). Esto detiene la ejecución. React intercepta esta Promesa y recuerda que es necesario repetir la representación del árbol de componentes después de que se cumpla la Promesa arrojada.
Este no es un efecto algebraico en sí mismo, aunque la creación de este truco fue inspirado por ellos. Este truco logra el mismo objetivo: algunos de los códigos a continuación en la pila de llamadas son temporalmente inferiores a algo más alto en la pila de llamadas (en este caso, Reaccionar), mientras que todas las funciones intermedias no tienen que saberlo o ser "envenenadas" por asíncronos o generadores. Por supuesto, no podemos "en realidad" reanudar la ejecución en JavaScript, pero desde el punto de vista de React, volver a mostrar el árbol de componentes después del permiso Promise es casi lo mismo. ¡Puedes hacer trampa cuando tu modelo de programación asume idempotencia!
Los ganchos son otro ejemplo que puede recordarle los efectos algebraicos. Una de las primeras preguntas que hace la gente es: ¿dónde llama useState a "saber" a qué componente se refiere?
function LikeButton() {
Ya expliqué esto al final de este artículo : en el objeto React hay un estado mutable "despachador actual", que indica la implementación que está utilizando actualmente (por ejemplo, en react-dom
). Del mismo modo, hay una propiedad de componente actual que apunta a la estructura de datos interna LikeButton. Así es como useState descubre qué hacer.
Antes de acostumbrarse, la gente suele pensar que parece un truco sucio por una razón obvia. Es incorrecto confiar en un estado mutable general. (Nota: ¿cómo cree que se implementa try / catch en el motor de JavaScript?)
Sin embargo, conceptualmente puede considerar useState () como un efecto de la ejecución de State (), que es procesada por React cuando se ejecuta su componente. Esto "explica" por qué React (lo que llama su componente) puede proporcionarle estado (es más alto en la pila de llamadas, por lo que puede proporcionar un controlador de efectos). De hecho, la implementación explícita del estado es uno de los ejemplos más comunes en los libros de texto sobre efectos algebraicos que he encontrado.
Nuevamente, por supuesto, no es así como React realmente funciona, porque no tenemos efectos algebraicos en JavaScript. En cambio, hay un campo oculto en el que guardamos el componente actual, así como un campo que apunta al "despachador" actual con la implementación useState. Como una optimización del rendimiento, incluso hay implementaciones useState separadas para montajes y actualizaciones . Pero si ahora está muy retorcido por este código, puede considerarlos manejadores de efectos ordinarios.
En resumen, podemos decir que en JavaScript throw
puede funcionar como una primera aproximación para los efectos de E / S (siempre que el código se pueda volver a ejecutar de forma segura más adelante, y siempre que no esté vinculado a la CPU), y el campo variable " el despachador "restaurado en prueba / finalmente puede servir como una aproximación aproximada para los manejadores de efectos síncronos.
Puede obtener una implementación de efectos de mayor calidad utilizando generadores , pero esto significa que tendrá que abandonar la naturaleza "transparente" de las funciones de JavaScript y tendrá que hacer todo con los generadores. Y esto es "bueno, eso ..."
Dónde encontrar más
Personalmente, me sorprendió cuánto sentido adquirieron los efectos algebraicos para mí. Siempre hice todo lo posible para comprender conceptos abstractos, como las mónadas, pero los efectos algebraicos simplemente se activaron en la cabeza. Espero que este artículo les ayude a "unirse" con usted.
No sé si alguna vez comenzarán a usarse a granel. Creo que me decepcionará si no se arraigan en ninguno de los idiomas principales para 2025. ¡Recuérdame verificar en cinco años!
Estoy seguro de que puedes hacer mucho más interesante con ellos, pero es realmente difícil sentir su fuerza hasta que comienzas a escribir código y usarlos. Si esta publicación despertó su curiosidad, aquí hay algunos recursos más donde puede leer con más detalle:
Muchas personas también han señalado que si omite el aspecto de mecanografía (como hice en este artículo), puede encontrar un uso anterior de dicha técnica en un sistema de condición en Common Lisp. , , call/cc .