Manejo de efectos secundarios sucios en código JavaScript puro y funcional

Si prueba suerte en la programación funcional, significa que pronto se encontrará con el concepto de funciones puras. A medida que continúe, encontrará que los programadores que prefieren un estilo funcional parecen estar obsesionados con estas características. Dicen que las funciones puras le permiten hablar sobre código. Dicen que las funciones puras son entidades que es poco probable que funcionen de manera tan impredecible que conducirán a una guerra termonuclear. También puede aprender de tales programadores que las funciones puras proporcionan transparencia referencial. Y así, hasta el infinito.

Por cierto, los programadores funcionales tienen razón. Las funciones puras son buenas. Pero hay un problema ...


El autor del material, cuya traducción presentamos a su atención, quiere hablar sobre cómo tratar los efectos secundarios en funciones puras.

El problema de las funciones puras.


Una función pura es una función que no tiene efectos secundarios (de hecho, esta no es una definición completa de una función pura, pero volveremos a esta definición). Sin embargo, si al menos entiendes algo en programación, entonces sabes que lo más importante aquí son precisamente los efectos secundarios. ¿Por qué calcular el número Pi hasta el centésimo decimal si nadie puede leer este número? Para mostrar algo en la pantalla o imprimir en una impresora, o presentarlo de alguna otra forma, accesible para la percepción, necesitamos llamar al comando apropiado desde el programa. ¿Y de qué sirven las bases de datos si no se les puede escribir nada? Para garantizar el funcionamiento de las aplicaciones, debe leer los datos de los dispositivos de entrada y solicitar información de los recursos de la red. Todo esto no se puede hacer sin efectos secundarios. Pero a pesar de este estado de cosas, la programación funcional se basa en funciones puras. Entonces, ¿cómo logran resolver esta paradoja los programadores que escriben programas en un estilo funcional?

Si responde esta pregunta en pocas palabras, los programadores funcionales hacen lo mismo que los matemáticos: hacen trampa. Aunque, a pesar de esta acusación, hay que decir que, desde un punto de vista técnico, simplemente siguen ciertas reglas. Pero encuentran lagunas en estas reglas y las expanden a tamaños increíbles. Lo hacen de dos maneras principales:

  1. Aprovechan la inyección de dependencia. Lo llamo arrojar un problema sobre una cerca.
  2. Usan functores, lo que me parece una forma extrema de dilación. Cabe señalar aquí que en Haskell se llama "IO functor" o "IO monad ", en PureScript se usa el término "Efecto", que, en mi opinión , es un poco mejor para describir la esencia de los functors.

Inyección de dependencia


La inyección de dependencia es la primera forma de lidiar con los efectos secundarios. Con este enfoque, tomamos todo lo que contamina el código y lo colocamos en los parámetros de la función. Entonces podemos considerar todo esto como algo que es parte de la responsabilidad de alguna otra función. Explicaré esto con el siguiente ejemplo:

// logSomething :: String -> String function logSomething(something) {    const dt = (new Date())toISOString();    console.log(`${dt}: ${something}`);    return something; } 

Aquí me gustaría hacer una nota para aquellos que están familiarizados con las firmas tipo. Si nos apegáramos estrictamente a las reglas, entonces tendríamos que tener en cuenta aquí los efectos secundarios. Pero nos ocuparemos de esto más tarde.

La función logSomething() tiene dos problemas que impiden que se declare limpia: crea un objeto Date y envía algo a la consola. Es decir, nuestra función no solo realiza operaciones de entrada-salida, sino que también produce, cuando se llama en diferentes momentos, diferentes resultados.

¿Cómo hacer que esta función sea limpia? Usando la técnica de inyección de dependencia, podemos tomar todo lo que contamina la función y hacer que funcione parámetros. Como resultado, en lugar de aceptar un parámetro, nuestra función aceptará tres parámetros:

 // logSomething: Date -> Console -> String -> * function logSomething(d, cnsl, something) {   const dt = d.toIsoString();   return cnsl.log(`${dt}: ${something}`); } 

Ahora, para llamar a la función, necesitamos transferirle todo lo que la contaminó antes:

 const something = "Curiouser and curiouser!" const d = new Date(); logSomething(d, console, something); //  "Curiouser and curiouser!" 

Aquí puede pensar que todo esto no tiene sentido, que solo subimos el problema un nivel hacia arriba y que esto no agregó pureza a nuestro código. Y sabes, estos son los pensamientos correctos. Esta es una escapatoria en su forma más pura.

Esto es como una ignorancia fingida: “No sabía que llamar al método de log del objeto cnsl conduciría a la ejecución de la declaración de E / S. Alguien me lo entregó, pero no sé de dónde vino todo ". Esta actitud está mal.

Y, de hecho, lo que está sucediendo no es tan estúpido como podría parecer a primera vista. logSomething() vistazo a las características de la función logSomething() . Si quieres hacer algo inmundo, debes hacerlo tú mismo. Digamos que puede pasar varios parámetros a esta función:

 const d = {toISOString: () => '1865-11-26T16:00:00.000Z'}; const cnsl = {   log: () => {       //      }, }; logSomething(d, cnsl, "Off with their heads!"); //   "Off with their heads!" 

Ahora nuestra función no hace nada (solo devuelve el parámetro something ). Pero ella es completamente pura. Si lo llama con los mismos parámetros varias veces, devolverá lo mismo cada vez. Y ese es todo el punto. Para que esta función sea impura, necesitamos realizar ciertas acciones intencionalmente. O, para decirlo de otra manera, todo de lo que depende una función está en su firma. No accede a ningún objeto global como console o Date . Esto formaliza todo.

Además, es importante tener en cuenta que podemos transferir otras funciones a nuestra función, que anteriormente no estaba limpia. Echa un vistazo a otro ejemplo. Imagine que de alguna forma hay un nombre de usuario y necesitamos obtener el valor del campo correspondiente de este formulario:

 // getUserNameFromDOM :: () -> String function getUserNameFromDOM() {   return document.querySelector('#username').value; } const username = getUserNameFromDOM(); username; //   "mhatter" 

En este caso, estamos tratando de cargar cierta información del DOM. Las funciones puras no hacen esto, ya que el document es un objeto global que puede cambiar en cualquier momento. Una forma de limpiar dicha función es pasarle el objeto de document global como parámetro. Sin embargo, aún puede pasarle la función querySelector() . Se ve así:

 // getUserNameFromDOM :: (String -> Element) -> String function getUserNameFromDOM($) {   return $('#username').value; } // qs :: String -> Element const qs = document.querySelector.bind(document); const username = getUserNameFromDOM(qs); username; //   "mhatter" 

Aquí, nuevamente, puedes pensar que esto es estúpido. Después de todo, aquí simplemente eliminamos de la función getUsernameFromDOM() lo que no nos permite llamarlo limpio. Sin embargo, no nos deshicimos de esto, solo transferimos la llamada al DOM a otra función, qs() . Puede parecer que el único resultado notable de este paso fue que el nuevo código era más largo que el anterior. En lugar de una función impura, ahora tenemos dos funciones, una de las cuales sigue siendo impura.

Espera un poco Imagine que necesitamos escribir una prueba para la función getUserNameFromDOM() . Ahora, comparando las dos opciones para esta función, piense en cuál será más fácil trabajar con ella. Para que la versión sucia de la función funcione, necesitamos un objeto de documento global. Además, este documento debe tener un elemento con el identificador de username . Si necesita probar una función similar fuera del navegador, deberá usar algo como JSDOM o un navegador sin una interfaz de usuario. Tenga en cuenta que todo esto es necesario solo para probar una función pequeña con una longitud de varias líneas. Y para probar la segunda versión limpia de esta función, es suficiente hacer lo siguiente:

 const qsStub = () => ({value: 'mhatter'}); const username = getUserNameFromDOM(qsStub); assert.strictEqual('mhatter', username, `Expected username to be ${username}`); 

Esto, por supuesto, no significa que para probar tales funciones, las pruebas de integración realizadas en un navegador real (o, al menos, usando algo como JSDOM) no sean necesarias. Pero este ejemplo demuestra algo muy importante, que es que ahora la función getUserNameFromDOM() vuelto completamente predecible. Si le pasamos qsStub() , siempre devolverá mhatter . La "imprevisibilidad" la hemos trasladado a la pequeña función qs() .

Si es necesario, podemos llevar mecanismos impredecibles a niveles aún más distantes de la función principal. Como resultado, podemos moverlos, relativamente hablando, a las "áreas fronterizas" del código. Esto dará como resultado que tengamos una capa delgada de código impuro que rodea un núcleo bien probado y predecible. La previsibilidad del código resulta extremadamente valiosa cuando crece el tamaño de los proyectos creados por programadores.

▍ Desventajas del mecanismo de inyección de dependencia


Usando la inyección de dependencia, puede escribir una aplicación grande y compleja. Sé esto, ya que yo mismo escribí una solicitud de este tipo . Con este enfoque, las pruebas se simplifican y las dependencias de funciones se vuelven claramente visibles. Pero la inyección de dependencia no está exenta de defectos. La principal es que cuando se usa, se pueden obtener firmas de funciones muy largas:

 function app(doc, con, ftch, store, config, ga, d, random) {   //     } app(document, console, fetch, store, config, ga, (new Date()), Math.random); 

De hecho, esto no es tan malo. Las desventajas de tales construcciones se manifiestan si algunos de los parámetros deben pasarse a ciertas funciones que están muy arraigadas en otras funciones. Parece la necesidad de pasar parámetros a través de muchos niveles de llamadas a funciones. Cuando aumenta el número de tales niveles, comienza a molestar. Por ejemplo, puede ser necesario transferir el objeto que representa la fecha a través de 5 funciones intermedias, mientras que ninguna de las funciones intermedias usa este objeto. Aunque, por supuesto, no se puede decir que tal situación sea algo así como una catástrofe universal. Además, esto permite ver claramente las dependencias de las funciones. Sin embargo, sea como sea, esto todavía no es tan agradable. Por lo tanto, consideramos el siguiente mecanismo.

▍ Funciones perezosas


Echemos un vistazo a la segunda escapatoria utilizada por los seguidores de la programación funcional. Consiste en la siguiente idea: un efecto secundario no es un efecto secundario hasta que realmente ocurre. Sé que eso suena misterioso. Para resolver esto, considere el siguiente ejemplo:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

Un ejemplo es quizás estúpido, lo sé. Si necesitamos el número 0, para que aparezca, simplemente ingréselo en el lugar correcto en el código. Y también sé que no escribirás código JavaScript para controlar las armas nucleares. Pero necesitamos este código para ilustrar la tecnología en cuestión.

Así que aquí hay un ejemplo de una función impura. Envía datos a la consola y también es la causa de la guerra nuclear. Sin embargo, imagine que necesitamos el cero que devuelve esta función. Imagine un escenario en el que necesitamos calcular algo después de lanzar un cohete. Digamos que podríamos tener que iniciar un temporizador de cuenta regresiva o algo así. En este caso, sería completamente natural pensar de antemano en hacer los cálculos. Y debemos asegurarnos de que el cohete se lance exactamente cuando sea necesario. No necesitamos realizar cálculos de tal manera que puedan conducir accidentalmente al lanzamiento de este cohete. Entonces, pensemos qué sucede si fZero() función fZero() en otra función que simplemente la devuelve. Digamos que será algo así como una envoltura de seguridad:

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   return fZero; } 

Puede llamar a la función returnZeroFunc() tantas veces como desee. En este caso, hasta que se lleve a cabo la implementación de lo que devuelve, nosotros (en teoría) estamos a salvo. En nuestro caso, esto significa que ejecutar el siguiente código no conducirá a una guerra nuclear:

 const zeroFunc1 = returnZeroFunc(); const zeroFunc2 = returnZeroFunc(); const zeroFunc3 = returnZeroFunc(); //     . 

Ahora un poco más estricto que antes, acerquémonos a la definición del término "función pura". Esto nos permitirá examinar la función returnZeroFunc() con más detalle. Entonces, la función está limpia bajo las siguientes condiciones:

  • No se observaron efectos secundarios.
  • Transparencia de enlace. Es decir, llamar a una función con los mismos valores de entrada siempre conduce a los mismos resultados.

returnZeroFunc() función returnZeroFunc() .

¿Tiene ella algún efecto secundario? Acabamos de descubrir que llamar a returnZeroFunc() no lanza misiles. Si no llama a lo que devuelve esta función, no pasará nada. Por lo tanto, podemos concluir que esta función no tiene efectos secundarios.

¿Es esta característica referencialmente transparente? Es decir, ¿siempre devuelve lo mismo cuando le pasa los mismos datos de entrada? Verificaremos esto, aprovechando el hecho de que en el fragmento de código anterior llamamos a esta función varias veces:

 zeroFunc1 === zeroFunc2; // true zeroFunc2 === zeroFunc3; // true 

Todo se ve bien, pero la función returnZeroFunc() aún no está completamente limpia. Ella se refiere a una variable que está fuera de su propio alcance. Para resolver este problema, reescribimos la función:

 // returnZeroFunc :: () -> (() -> Number) function returnZeroFunc() {   function fZero() {       console.log('Launching nuclear missiles');       //              return 0;   }   return fZero; } 

Ahora la función puede considerarse limpia. Sin embargo, en esta situación, las reglas de JavaScript juegan en nuestra contra. Es decir, ya no podemos usar el operador === para verificar la transparencia referencial de una función. Esto se debe al hecho de que returnZeroFunc() siempre devolverá una nueva referencia a la función. Es cierto que la transparencia del enlace se puede verificar examinando el código usted mismo. Tal análisis mostrará que con cada llamada a función devuelve un enlace a la misma función.

Ante nosotros hay una pequeña escapatoria ordenada. ¿Pero se puede usar en proyectos reales? La respuesta a esta pregunta es positiva. Sin embargo, antes de hablar sobre cómo usar esto en la práctica, desarrollaremos un poco nuestra idea. A saber, fZero() a la función peligrosa fZero() :

 // fZero :: () -> Number function fZero() {   console.log('Launching nuclear missiles');   //          return 0; } 

Intentaremos usar el cero devuelto por esta función, pero lo haremos para que (hasta ahora) no comience una guerra nuclear. Para hacer esto, cree una función que tome el cero devuelto por la función fZero() y le agregue uno:

 // fIncrement :: (() -> Number) -> Number function fIncrement(f) {   return f() + 1; } fIncrement(fZero); //      //   1 

Eso es mala suerte ... Accidentalmente comenzamos una guerra nuclear. Intentemos nuevamente, pero esta vez no devolveremos un número. En cambio, devolvemos una función que algún día devuelve un número:

 // fIncrement :: (() -> Number) -> (() -> Number) function fIncrement(f) {   return () => f() + 1; } fIncrement(zero); //   [Function] 

Ahora puedes respirar tranquilo. La catástrofe se evita. Seguimos el estudio. Gracias a estas dos funciones, podemos crear un montón de "números posibles":

 const fOne   = fIncrement(zero); const fTwo   = fIncrement(one); const fThree = fIncrement(two); //   … 

Además, podemos crear muchas funciones cuyos nombres comenzarán con f (llamémoslas funciones f*() ), diseñadas para trabajar con "números posibles":

 // fMultiply :: (() -> Number) -> (() -> Number) -> (() -> Number) function fMultiply(a, b) {   return () => a() * b(); } // fPow :: (() -> Number) -> (() -> Number) -> (() -> Number) function fPow(a, b) {   return () => Math.pow(a(), b()); } // fSqrt :: (() -> Number) -> (() -> Number) function fSqrt(x) {   return () => Math.sqrt(x()); } const fFour = fPow(fTwo, fTwo); const fEight = fMultiply(fFour, fTwo); const fTwentySeven = fPow(fThree, fThree); const fNine = fSqrt(fTwentySeven); //    ,   . ! 

¿Ves lo que hicimos aquí? Con los "números posibles" puede hacer lo mismo que con los números normales. Los matemáticos llaman a esto isomorfismo . Un número ordinario siempre se puede convertir en un "número posible" al colocarlo en una función. Puede obtener el "número posible" llamando a la función. En otras palabras, tenemos un mapeo entre números regulares y "números posibles". Esto, de hecho, es mucho más interesante de lo que parece. Pronto volveremos a esta idea.

La técnica anterior que utiliza la función de envoltura es una estrategia válida. Podemos escondernos detrás de las funciones tanto como sea necesario. Y, dado que todavía no hemos llamado a ninguna de estas funciones, todas ellas, en teoría, son puras. Y nadie comienza una guerra. En el código regular (no relacionado con cohetes), en realidad necesitamos efectos secundarios al final. Envolver todo lo que necesitamos en una función nos permite controlar con precisión estos efectos. Elegimos el momento en que aparecen estos efectos.

Cabe señalar que no es muy conveniente utilizar construcciones uniformes con montones de corchetes en todas partes para declarar funciones. Y crear nuevas versiones de cada función tampoco es una actividad agradable. JavaScript tiene algunas excelentes funciones Math.sqrt() como Math.sqrt() . Sería genial si hubiera una manera de utilizar estas funciones ordinarias con nuestros "valores pendientes". En realidad, hablaremos de esto ahora.

Efecto Functor


Aquí hablaremos sobre los functores representados por objetos que contienen nuestras "funciones diferidas". Para representar el functor, usaremos el objeto Effect . Pondremos nuestra función fZero() en dicho objeto. Pero, antes de hacer esto, haremos que esta función sea un poco más segura:

 // zero :: () -> Number function fZero() {   console.log('Starting with nothing');   //  , ,     .   //       .   return 0; } 

Ahora describimos la función constructora para crear objetos de tipo Effect :

 // Effect :: Function -> Effect function Effect(f) {   return {}; } 

No hay nada particularmente interesante aquí, así que trabajaremos en esta función. Por lo tanto, queremos usar la función fZero() habitual con el objeto Effect . Para proporcionar tal escenario, escribiremos un método que acepte una función regular y algún día lo aplique a nuestro "valor pendiente". Y haremos esto sin llamar a la función Effect . Llamamos a tal función map() . Tiene ese nombre debido al hecho de que crea un mapeo entre la función habitual y la función Effect . Puede verse así:

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       }   } } 

Ahora, si monitorea de cerca lo que está sucediendo, es posible que tenga preguntas sobre la función map() . Se ve sospechosamente similar a la canción. Volveremos a este problema más tarde, pero por ahora probaremos lo que tenemos en este momento en acción:

 const zero = Effect(fZero); const increment = x => x + 1; //   . const one = zero.map(increment); 

Entonces ... Ahora no tenemos la oportunidad de observar lo que sucedió aquí. Por lo tanto, modifiquemos Effect para, por así decirlo, tener la oportunidad de "apretar el gatillo":

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }   } } const zero = Effect(fZero); const increment = x => x + 1; //  . const one = zero.map(increment); one.runEffects(); //       //   1 

Si es necesario, podemos continuar llamando a la función map() :

 const double = x => x * 2; const cube = x => Math.pow(x, 3); const eight = Effect(fZero)   .map(increment)   .map(double)   .map(cube); eight.runEffects(); //       //   8 

Aquí, lo que está sucediendo ya comienza a ser más interesante. Lo llamamos "functor". Todo esto significa que el objeto Effect tiene una función map() y obedece algunas reglas . Sin embargo, estas no son reglas que prohíben algo. Estas reglas son sobre lo que puedes hacer. Son más como privilegios. Como el objeto Effect es un functor, obedece estas reglas. En particular, esta es la llamada "regla de composición".

Se ve así:

Si hay un objeto Effect llamado e y dos funciones, f y g , entonces e.map(g).map(f) equivalente a e.map(x => f(g(x))) .

En otras palabras, dos métodos map() consecutivos son equivalentes a componer dos funciones. Esto significa que un objeto de tipo Effect puede realizar acciones similares a las siguientes (recuerde uno de los ejemplos anteriores):

 const incDoubleCube = x => cube(double(increment(x))); //       Ramda  lodash/fp      : // const incDoubleCube = compose(cube, double, increment); const eight = Effect(fZero).map(incDoubleCube); 

Cuando hacemos lo que se muestra aquí, tenemos la garantía de obtener el mismo resultado que obtendríamos usando una versión de este código con una triple llamada a map() . Podemos usar esto al refactorizar el código, y podemos estar seguros de que el código funcionará correctamente. En algunos casos, cambiar un enfoque por otro puede incluso mejorar el rendimiento.

Ahora propongo dejar de experimentar con números y hablar sobre lo que se parece más al código utilizado en proyectos reales.

▍ Método de ()


El constructor del objeto Effect acepta, como argumento, una función. Esto es conveniente, ya que la mayoría de los efectos secundarios que queremos posponer son funciones. Por ejemplo, estos son Math.random() y console.log() . Sin embargo, a veces necesita poner un valor en un objeto Effect que no sea una función. , , window . , . , ( -, , , Haskell pure ):

 // of :: a -> Effect a Effect.of = function of(val) {   return Effect(() => val); } 

, , , -. , , . HTML- . , . . Por ejemplo:

 window.myAppConf = {   selectors: {       'user-bio':     '.userbio',       'article-list': '#articles',       'user-name':    '.userfullname',   },   templates: {       'greet':  'Pleased to meet you, {name}',       'notify': 'You have {n} alerts',   } }; 

, Effect.of() , Effect :

 const win = Effect.of(window); userBioLocator = win.map(x => x.myAppConf.selectors['user-bio']); //   Effect('.userbio') 

▍ Effect


. , Effect . , getElementLocator() , Effect , . DOM, document.querySelector() — , . :

 // $ :: String -> Effect DOMElement function $(selector) {   return Effect.of(document.querySelector(s)); } 

, , map() :

 const userBio = userBioLocator.map($); //   Effect(Effect(<div>)) 

, , . div , map() , , . , innerHTML , :

 const innerHTML = userBio.map(eff => eff.map(domEl => domEl.innerHTML)); //   Effect(Effect('<h2>User Biography</h2>')) 

, . userBio , . , , , . , , Effect('user-bio') . , , , :

 Effect(() => '.userbio'); 

— . :

 Effect(() => window.myAppConf.selectors['user-bio']); 

, map() , ( ). , , $ , :

 Effect(() => $(window.myAppConf.selectors['user-bio'])); 

, :

 Effect(   () => Effect.of(document.querySelector(window.myAppConf.selectors['user-bio']))) ); 

Effect.of , :

 Effect(   () => Effect(       () => document.querySelector(window.myAppConf.selectors['user-bio'])   ) ); 

, , , . Effect .

▍ join()


? , Effect . , , .

Effect .runEffect() . . , - , , , , . , . join() . Effect , runEffect() , . , .

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }   } } 

, :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .map($)   .join()   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

▍ chain()


, .map() , .join() , . , , . , , Effect . , .map() .join() . , , Effect :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }   } } 

chain() - , , Effect ( , ). HTML- :

 const userBioHTML = Effect.of(window)   .map(x => x.myAppConf.selectors['user-bio'])   .chain($)   .map(x => x.innerHTML); //   Effect('<h2>User Biography</h2>') 

-. . , flatMap . , , — , , join() . Haskell, , bind . , - , , chain , flatMap bind — .

▍ Effect


Effect , . . , DOM, , ? , , , . , . — .

 // tpl :: String -> Object -> String const tpl = curry(function tpl(pattern, data) {   return Object.keys(data).reduce(       (str, key) => str.replace(new RegExp(`{${key}}`, data[key]),       pattern   ); }); 

. :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); //   Effect({name: 'Mr. Hatter'}); const pattern = win.map(w => w.myAppConfig.templates('greeting')); //   Effect('Pleased to meet you, {name}'); 

, . . ( name pattern ) Effect . tpl() , , Effect .
, map() Effect tpl() :

 pattern.map(tpl); //   Effect([Function]) 

, . map() :

 map :: Effect a ~> (a -> b) -> Effect b 

:

 tpl :: String -> Object -> String 

, map() pattern , ( , tpl() ) Effect .

 Effect (Object -> String) 

pattern Effect . . Effect , . ap() :

 // Effect :: Function -> Effect function Effect(f) {   return {       map(g) {           return Effect(x => g(f(x)));       },       runEffects(x) {           return f(x);       }       join(x) {           return f(x);       }       chain(g) {           return Effect(f).map(g).join();       }       ap(eff) {            //  -  ap,    ,   eff   (  ).           //    map  ,    eff       (  'g')           //   g,     f()           return eff.map(g => g(f()));       }   } } 

.ap() :

 const win = Effect.of(window); const name = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str})); const pattern = win.map(w => w.myAppConfig.templates('greeting')); const greeting = name.ap(pattern.map(tpl)); //   Effect('Pleased to meet you, Mr Hatter') 

, … , , .ap() . , , map() , ap() . , , .

. , . , , , Effect , ap() . , :

 // liftA2 :: (a -> b -> c) -> (Applicative a -> Applicative b -> Applicative c) const liftA2 = curry(function liftA2(f, x, y) {   return y.ap(x.map(f));   //      :   // return x.map(f).chain(g => y.map(g)); }); 

liftA2() , , . liftA3() :

 // liftA3 :: (a -> b -> c -> d) -> (Applicative a -> Applicative b -> Applicative c -> Applicative d) const liftA3 = curry(function liftA3(f, a, b, c) {   return c.ap(b.ap(a.map(f))); }); 

, liftA2() liftA3() Effect . , , ap() .

liftA2() :

 const win = Effect.of(window); const user = win.map(w => w.myAppConfig.selectors['user-name'])   .chain($)   .map(el => el.innerHTML)   .map(str => ({name: str}); const pattern = win.map(w => w.myAppConfig.templates['greeting']); const greeting = liftA2(tpl)(pattern, user); //   Effect('Pleased to meet you, Mr Hatter') 

?


, , , . ? , Effect ap() . , ? ?

: « , , ».

:

  • — ?
  • , Effect , ?


— . , , , . const pattern = window.myAppConfig.templates['greeting']; , , , :

 const pattern = Effect.of(window).map(w => w.myAppConfig.templates('greeting')); 

— , , , , . . — , , . , , , , , , . , . — . , , , . , .

. .

▍ Effect


, , . - Facebook Gmail . ? .

, . . CSV- . . , , , . , . , . , , , .

, . , map() reduce() , . . , . , , , . 4 (, , 8, 16, ). , , . , . , - .

, , . , . ¿No se parece a nada? , , , . . , .

TensorFlow , .

TensorFlow, , . «». , , :

 node1 = tf.constant(3.0, tf.float32) node2 = tf.constant(4.0, tf.float32) node3 = tf.add(node1, node2) 

Python, JavaScript. , Effect , add() , ( sess.run() ).

 print("node3: ", node3) print("sess.run(node3): ", sess.run(node3)) #  node3:  Tensor("Add_2:0", shape=(), dtype=float32) #  sess.run(node3):  7.0 

, (7.0) , sess.run() . , . , , , .

Resumen


, . , . Effect .
, , , , , . , , . Effect , , , . , .

— . , . , , . . . , .



Estimados lectores! ?

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


All Articles