Lidando com efeitos colaterais sujos no código JavaScript puro e funcional

Se você tentar a programação funcional, significa que em breve encontrará o conceito de funções puras. Ao continuar, você descobrirá que os programadores que preferem um estilo funcional parecem obcecados por esses recursos. Eles dizem que funções puras permitem que você fale sobre código. Eles dizem que funções puras são entidades que dificilmente funcionam tão imprevisivelmente que levarão a uma guerra termonuclear. Você também pode aprender com esses programadores que funções puras fornecem transparência referencial. E assim - ao infinito.

A propósito, programadores funcionais estão certos. Funções puras são boas. Mas há um problema ...


O autor do material, cuja tradução apresentamos à sua atenção, quer falar sobre como lidar com os efeitos colaterais em funções puras.

O problema das funções puras


Uma função pura é uma função que não tem efeitos colaterais (na verdade, essa não é uma definição completa de uma função pura, mas retornaremos a essa definição). No entanto, se você pelo menos entende alguma coisa na programação, sabe que a coisa mais importante aqui são precisamente os efeitos colaterais. Por que calcular o número Pi até a centésima casa decimal se ninguém pode ler esse número? Para exibir algo na tela ou imprimir em uma impressora ou apresentá-lo de alguma outra forma, acessível para percepção, precisamos chamar o comando apropriado do programa. E qual é a utilidade dos bancos de dados se nada puder ser escrito neles? Para garantir a operação dos aplicativos, você precisa ler os dados dos dispositivos de entrada e solicitar informações dos recursos da rede. Tudo isso não pode ser feito sem efeitos colaterais. Mas, apesar desse estado de coisas, a programação funcional é construída em torno de funções puras. Então, como os programadores que escrevem programas em um estilo funcional conseguem resolver esse paradoxo?

Se você responder a essa pergunta em poucas palavras, os programadores funcionais fazem o mesmo que os matemáticos: eles trapaceiam. Embora, apesar dessa acusação, deva-se dizer que eles, do ponto de vista técnico, simplesmente seguem certas regras. Mas eles encontram brechas nessas regras e as expandem para tamanhos incríveis. Eles fazem isso de duas maneiras principais:

  1. Eles aproveitam a injeção de dependência. Eu chamo isso de jogar um problema por cima de uma cerca.
  2. Eles usam functores, o que me parece uma forma extrema de procrastinação. Deve-se notar aqui que, em Haskell, é chamado de "IO functor" ou "IO monad"; no PureScript, o termo "Effect" é usado, o que, na minha opinião , é um pouco melhor para descrever a essência dos functores.

Injeção de Dependência


A injeção de dependência é a primeira maneira de lidar com os efeitos colaterais. Usando essa abordagem, pegamos tudo o que polui o código e o colocamos nos parâmetros da função. Então, podemos considerar tudo isso como algo que faz parte da responsabilidade de alguma outra função. Vou explicar isso com o seguinte exemplo:

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

Aqui, gostaria de fazer uma anotação para aqueles que estão familiarizados com as assinaturas de tipo. Se seguíssemos rigorosamente as regras, teríamos que levar em conta aqui os efeitos colaterais. Mas vamos lidar com isso mais tarde.

A função logSomething() tem dois problemas que impedem que ela seja declarada limpa: cria um objeto Date e gera algo no console. Ou seja, nossa função não apenas realiza operações de entrada e saída, mas também produz, quando é chamada em momentos diferentes, resultados diferentes.

Como limpar esta função? Usando a técnica de injeção de dependência, podemos pegar tudo o que polui a função e torná-la parâmetros de função. Como resultado, em vez de aceitar um parâmetro, nossa função aceitará três parâmetros:

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

Agora, para chamar a função, precisamos transferir tudo para ela que a poluiu antes:

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

Aqui você pode pensar que tudo isso não faz sentido, que apenas aumentamos o problema um nível acima, e isso não adicionou pureza ao nosso código. E você sabe, esses são os pensamentos certos. Esta é uma brecha na sua forma mais pura.

Isso é como um analfabetismo falso: “Eu não sabia que chamar o método log do objeto cnsl levaria à execução da instrução de E / S. Alguém acabou de me entregar, mas eu não sei de onde tudo veio. ” Essa atitude está errada.

E, de fato, o que está acontecendo não é tão estúpido quanto parece à primeira vista. logSomething() os recursos da função logSomething() . Se você deseja fazer algo impuro, deve fazê-lo você mesmo. Digamos que você possa passar vários parâmetros para esta função:

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

Agora nossa função não faz nada (apenas retorna o parâmetro something ). Mas ela é completamente pura. Se você chamá-lo com os mesmos parâmetros várias vezes, ele retornará a mesma coisa todas as vezes. E esse é o ponto. Para tornar essa função impura, precisamos executar intencionalmente determinadas ações. Ou, em outras palavras, tudo o que uma função depende está em sua assinatura. Ele não acessa nenhum objeto global como console ou Date . Isso formaliza tudo.

Além disso, é importante observar que podemos transferir outras funções para nossa função, que não estavam limpas anteriormente. Veja outro exemplo. Imagine que, de alguma forma, exista um nome de usuário e precisamos obter o valor do campo correspondente deste formulário:

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

Nesse caso, estamos tentando carregar algumas informações do DOM. As funções puras não fazem isso, pois o document é um objeto global que pode ser alterado a qualquer momento. Uma maneira de limpar essa função é passar o objeto de document global como parâmetro. No entanto, você ainda pode passar a função querySelector() para querySelector() . É assim:

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

Aqui, novamente, você pode pensar que isso é estúpido. Afinal, aqui simplesmente removemos da função getUsernameFromDOM() o que não nos permite chamá-la de limpa. No entanto, não nos livramos disso, apenas transferindo a chamada para o DOM para outra função, qs() . Pode parecer que o único resultado perceptível dessa etapa foi que o novo código era mais longo que o antigo. Em vez de uma função impura, agora temos duas funções, uma das quais ainda é imunda.

Espere um pouco. Imagine que precisamos escrever um teste para a função getUserNameFromDOM() . Agora, comparando as duas opções para essa função, pense em qual delas será mais fácil trabalhar? Para que a versão suja da função funcione, precisamos de um objeto de documento global. Além disso, este documento deve ter um elemento com o identificador de username . Se você precisar testar uma função semelhante fora do navegador, precisará usar algo como JSDOM ou um navegador sem uma interface com o usuário. Observe que tudo isso é necessário apenas para testar uma função pequena com um comprimento de várias linhas. E, para testar a segunda versão limpa dessa função, basta fazer o seguinte:

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

Obviamente, isso não significa que, para testar essas funções, não sejam necessários testes de integração executados em um navegador real (ou, pelo menos, usando algo como JSDOM). Mas este exemplo demonstra uma coisa muito importante, que é agora que a função getUserNameFromDOM() tornou completamente previsível. Se passarmos qsStub() para ele, ele sempre retornará mhatter . A "imprevisibilidade" que passamos para a pequena função qs() .

Se necessário, podemos levar mecanismos imprevisíveis a níveis ainda mais distantes da função principal. Como resultado, podemos movê-los, relativamente falando, para as “áreas de fronteira” do código. Isso resultará em uma fina camada de código impuro que envolve um kernel bem testado e previsível. A previsibilidade do código acaba sendo extremamente valiosa quando o tamanho dos projetos criados pelos programadores aumenta.

▍ Desvantagens do mecanismo de injeção de dependência


Usando a injeção de dependência, você pode gravar um aplicativo grande e complexo. Eu sei disso, já que eu próprio escrevi esse pedido . Com essa abordagem, o teste é simplificado e as dependências de funções se tornam claramente visíveis. Mas a injeção de dependência não está isenta de falhas. O principal é que, quando usado, assinaturas de funções muito longas podem ser obtidas:

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

De fato, isso não é tão ruim. As desvantagens de tais construções se manifestam se alguns dos parâmetros precisarem ser passados ​​para determinadas funções que estão profundamente embutidas em outras funções. Parece que é necessário passar parâmetros por vários níveis de chamadas de função. Quando o número de tais níveis aumenta, começa a incomodar. Por exemplo, pode ser necessário transferir o objeto que representa a data através de 5 funções intermediárias, enquanto nenhuma das funções intermediárias usa esse objeto. Embora, é claro, não se possa dizer que tal situação seja algo como uma catástrofe universal. Além disso, isso possibilita ver claramente as dependências das funções. No entanto, seja como for, isso ainda não é tão agradável. Portanto, consideramos o seguinte mecanismo.

Functions funções preguiçosas


Vamos dar uma olhada na segunda brecha usada pelos adeptos da programação funcional. Consiste na seguinte idéia: um efeito colateral não é um efeito colateral até que realmente ocorra. Eu sei que isso soa misterioso. Para descobrir isso, considere o seguinte exemplo:

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

Um exemplo talvez seja estúpido, eu sei disso. Se precisarmos do número 0, para que ele apareça, basta inseri-lo no lugar certo no código. E também sei que você não escreverá código JavaScript para controlar armas nucleares. Mas precisamos desse código para ilustrar a tecnologia em questão.

Então, aqui está um exemplo de uma função impura. Ele envia dados para o console e também é a causa da guerra nuclear. No entanto, imagine que precisamos do zero que essa função retorna. Imagine um cenário em que precisamos calcular algo após o lançamento de um foguete. Digamos que possamos precisar iniciar um temporizador de contagem regressiva ou algo assim. Nesse caso, seria completamente natural pensar antecipadamente em fazer os cálculos. E devemos garantir que o foguete seja lançado exatamente quando necessário. Não precisamos realizar cálculos de forma que eles possam acidentalmente levar ao lançamento deste foguete. Então, vamos pensar no que acontece se envolvermos a função fZero() em outra função que simplesmente a retorna. Digamos que será algo como um wrapper de segurança:

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

Você pode chamar a função returnZeroFunc() quantas vezes quiser. Nesse caso, até que a implementação do que ele retorna seja realizada, nós (teoricamente) estamos seguros. No nosso caso, isso significa que a execução do código a seguir não levará a uma guerra nuclear:

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

Agora um pouco mais rigoroso do que antes, vamos abordar a definição do termo "função pura". Isso nos permitirá examinar a função returnZeroFunc() com mais detalhes. Portanto, a função é limpa nas seguintes condições:

  • Sem efeitos colaterais observados.
  • Transparência do link. Ou seja, chamar uma função com os mesmos valores de entrada sempre leva aos mesmos resultados.

returnZeroFunc() analisar a função returnZeroFunc() .

Ela tem algum efeito colateral? Acabamos de descobrir que chamar returnZeroFunc() não lança mísseis. Se você não chamar o que essa função retorna, nada acontecerá. Portanto, podemos concluir que essa função não tem efeitos colaterais.

Esse recurso é referencialmente transparente? Ou seja, ele sempre retorna o mesmo ao passar os mesmos dados de entrada para ele? Vamos verificar isso, aproveitando o fato de que, no fragmento de código acima, chamamos essa função várias vezes:

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

Tudo parece bom, mas a função returnZeroFunc() ainda não está completamente limpa. Ela se refere a uma variável que está fora de seu próprio escopo. Para resolver esse problema, reescrevemos a função:

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

Agora a função pode ser considerada limpa. No entanto, nessa situação, as regras de JavaScript jogam contra nós. Ou seja, não podemos mais usar o operador === para verificar a transparência referencial de uma função. Isso se deve ao fato de returnZeroFunc() sempre retornar uma nova referência para a função. É verdade que a transparência do link pode ser verificada examinando você mesmo o código. Essa análise mostrará que, a cada chamada de função, ele retorna um link para a mesma função.

Diante de nós está uma pequena brecha. Mas pode ser usado em projetos reais? A resposta a esta pergunta é positiva. No entanto, antes de falar sobre como usar isso na prática, desenvolveremos um pouco nossa idéia. Ou seja, fZero() retornar à função perigosa fZero() :

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

Vamos tentar usar o zero retornado por essa função, mas faremos isso para que (até agora) uma guerra nuclear não comece. Para fazer isso, crie uma função que fZero() o zero retornado pela função fZero() e adicione uma a ela:

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

Isso é azar ... Começamos acidentalmente uma guerra nuclear. Vamos tentar novamente, mas desta vez não retornaremos um número. Em vez disso, retornamos uma função que um dia retorna um número:

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

Agora você pode respirar com calma. A catástrofe é evitada. Continuamos o estudo. Graças a essas duas funções, podemos criar um monte de "números possíveis":

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

Além disso, podemos criar muitas funções cujos nomes começarão com f (vamos chamá-los de funções f*() ), projetados para trabalhar com "números possíveis":

 // 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); //    ,   . ! 

Viu o que fizemos aqui? Com "números possíveis", você pode fazer o mesmo que com números comuns. Os matemáticos chamam isso de isomorfismo . Um número comum sempre pode ser transformado em um "número possível", colocando-o em uma função. Você pode obter o "número possível" chamando a função. Em outras palavras, temos um mapeamento entre números regulares e "números possíveis". Na verdade, isso é muito mais interessante do que parece. Em breve retornaremos a essa ideia.

A técnica acima, usando a função wrapper, é uma estratégia válida. Podemos nos esconder atrás das funções, tanto quanto necessário. E, como ainda não chamamos nenhuma dessas funções, todas elas, teoricamente, são puras. E ninguém começa uma guerra. No código regular (não relacionado a foguetes), na verdade precisamos de efeitos colaterais no final. Agrupar tudo o que precisamos em uma função nos permite controlar com precisão esses efeitos. Escolhemos a hora em que esses efeitos aparecem.

Deve-se notar que não é muito conveniente usar construções uniformes com montes de colchetes em todos os lugares para declarar funções. E criar novas versões de cada função também não é uma atividade agradável. O JavaScript possui ótimas funções Math.sqrt() como Math.sqrt() . Seria ótimo se houvesse uma maneira de usar essas funções comuns com nossos "valores pendentes". Na verdade, falaremos sobre isso agora.

Efeito Functor


Aqui falaremos sobre functores representados por objetos que contêm nossas "funções adiadas". Para representar o functor, usaremos o objeto Effect . Vamos colocar nossa função fZero() em um objeto desse tipo. Mas, antes de fazer isso, tornaremos essa função um pouco mais segura:

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

Agora, descrevemos a função construtora para criar objetos do tipo Effect :

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

Não há nada particularmente interessante aqui, portanto, trabalharemos nesse recurso. Então, queremos usar a função fZero() usual com o objeto Effect . Para fornecer esse cenário, escreveremos um método que aceita uma função regular e um dia o aplicará ao nosso "valor pendente". E faremos isso sem chamar a função Effect . Chamamos esse map() função map() . Ele tem esse nome devido ao fato de criar um mapeamento entre a função usual e a função Effect . Pode ser assim:

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

Agora, se você acompanhar de perto o que está acontecendo, poderá ter perguntas sobre a função map() . Parece suspeitamente semelhante à música. Voltaremos a esse problema mais tarde, mas por enquanto testaremos o que temos no momento em ação:

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

Então ... Agora não temos a oportunidade de observar o que aconteceu aqui. Portanto, vamos modificar o Effect para, por assim dizer, ter a oportunidade de "puxar o gatilho":

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

Se necessário, podemos continuar chamando a função 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 

Aqui, o que está acontecendo já está começando a se tornar mais interessante. Chamamos isso de "functor". Tudo isso significa que o objeto Effect tem uma função map() e obedece a algumas regras . No entanto, essas não são regras que proíbem qualquer coisa. Essas regras são sobre o que você pode fazer. Eles são mais como privilégios. Como o objeto Effect é um functor, ele obedece a essas regras. Em particular, esta é a chamada "regra de composição".

É assim:

Se houver um objeto Effect chamado e e duas funções, f e g , e.map(g).map(f) equivalente a e.map(x => f(g(x))) .

Em outras palavras, dois métodos map() consecutivos são equivalentes a compor duas funções. Isso significa que um objeto do tipo Effect pode executar ações semelhantes às seguintes (lembre-se de um dos exemplos acima):

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

Quando fazemos o que é mostrado aqui, temos a garantia de obter o mesmo resultado que obteríamos usando uma versão desse código com uma chamada tripla para map() . Podemos usar isso ao refatorar o código e podemos ter certeza de que o código funcionará corretamente. Em alguns casos, alterar uma abordagem para outra pode até melhorar o desempenho.

Agora, proponho parar de experimentar números e falar sobre o que parece mais com o código usado em projetos reais.

▍Método de ()


O construtor do objeto Effect aceita, como argumento, uma função. Isso é conveniente, pois a maioria dos efeitos colaterais que queremos adiar são funções. Por exemplo, estes são Math.random() e console.log() . No entanto, às vezes você precisa colocar um valor em um objeto Effect que não seja uma função. , , window . , . , ( -, , , Haskell pure ):

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

, , , -. , , . HTML- . , . . Por exemplo:

 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, ). , , . , . , - .

, , . , . Não se parece com 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() . , . , , , .

Sumário


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

— . , . , , . . . , .



Caros leitores! ?

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


All Articles