Programação funcional do ponto de vista do EcmaScript. Composição, Caril, Aplicação Parcial

Olá Habr!

Hoje, continuamos nossa pesquisa sobre programação funcional no contexto do EcmaScript, cuja especificação é baseada em JavaScript. No artigo anterior, examinamos os conceitos básicos: funções puras, lambdas, o conceito de imunidade. Hoje falaremos sobre técnicas de FP um pouco mais complexas: composição, currying e funções puras. O artigo foi escrito no estilo de "pseudo codreview", ou seja, resolveremos um problema prático, enquanto estudamos os conceitos de transições de fase e o código de refatoração para aproximar o último dos ideais das transições de fase.

Então, vamos começar!

Suponha que tenhamos uma tarefa: criar um conjunto de ferramentas para trabalhar com palíndromos.
Palíndromo
Sexo masculino
Uma palavra ou frase lida da esquerda para a direita e da direita para a esquerda.
"P. "Eu vou com a espada do juiz"
Uma das implementações possíveis desta tarefa pode ser assim:

function getPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase().split('').reverse().join(''); // -       ,       return str; } function isPalindrom (str) { const regexp = /[\.,\/#!$%\^&\*;:{}=\-_`~()?\s]/g; str = str.replace(regexp, '').toLowerCase(); return str === str.split('').reverse().join(''); } 

Obviamente, essa implementação funciona. Podemos esperar que o getPalindrom funcione corretamente se a API retornar os dados corretos. Uma chamada para isPalindrom ('eu vou com um juiz de espada') retornará verdadeiro e uma chamada para isPalindrom ('não um palíndromo') retornará falso. Essa implementação é boa em termos de ideais de programação funcional? Definitivamente não é bom!

De acordo com a definição de Funções puras deste artigo :
Funções puras (PF) - sempre retornam um resultado previsto.
Propriedades PF

O resultado da execução do PF depende apenas dos argumentos passados ​​e do algoritmo que implementa o PF
Não use valores globais
Não modifique valores externos ou argumentos passados
Não grave dados em arquivos, bancos de dados ou em qualquer outro lugar
E o que vemos em nosso exemplo com palíndromos?

Em primeiro lugar, há duplicação de código, ou seja, o princípio de DRY é violado. Em segundo lugar, a função getPalindrom acessa o banco de dados. Terceiro, as funções modificam seus argumentos. Total, nossas funções não são limpas.

Lembre-se da definição: programação funcional é uma maneira de escrever código através da compilação de um conjunto de funções.

Nós compomos um conjunto de funções para esta tarefa:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g;//(1) const replace = (regexp, replacement, str) => str.replace(regexp, replacement);//(2) const toLowerCase = str => str.toLowerCase();//(3) const stringReverse = str => str.split('').reverse().join('');//(4) const isStringsEqual = (strA, strB) => strA === strB;//(5) 

Na linha 1, declaramos a expressão regular constante em forma funcional. Este método de descrição de constantes é frequentemente usado no FP. Na linha 2, encapsulamos o método String.prototype.replace em uma abstração de substituição funcional para que ele (a chamada de substituição) esteja em conformidade com o contrato de programação funcional. Na linha 3, uma abstração para String.prototype.toLowerCase foi criada da mesma maneira. No quarto, eles implementaram uma função que cria uma nova string expandida a partir da passada. 5ª verifica a igualdade das strings.

Observe que nossos recursos são extremamente limpos! Falamos sobre os benefícios de funções puras em um artigo anterior.

Agora precisamos implementar uma verificação para ver se a string é um palíndromo. Uma composição de funções virá em nosso auxílio.

A composição de funções é a união de duas ou mais funções em uma determinada função resultante que implementa o comportamento daquelas combinadas na sequência algorítmica desejada.

A definição pode parecer complicada, mas, do ponto de vista prático, é justa.

Nós podemos fazer isso:

 isStringsEqual(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')), stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')))); 

ou assim:

 const strA = toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    ')); const strB = stringReverse(toLowerCase(replace(allNotWordSymbolsRegexpGlobal(), '', '    '))); console.log(isStringsEqual(strA, strB)); 

ou insira outro conjunto de variáveis ​​explicativas para cada etapa do algoritmo implementado. Esse código geralmente pode ser visto em projetos, e este é um exemplo típico de composição - passar uma chamada para uma função como argumento para outra. No entanto, como vemos, em uma situação em que há muitas funções, essa abordagem é ruim, porque este código não é legível! E agora? Bem, sua programação funcional, discordamos?

De fato, como é geralmente o caso na programação funcional, só precisamos escrever outra função.

 const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); 

A função de composição pega uma lista de funções executáveis ​​como argumentos, as transforma em uma matriz, as armazena em um fechamento e retorna uma função que espera um valor inicial. Depois que o valor inicial é passado, a execução seqüencial de todas as funções da matriz fns é iniciada. O argumento da primeira função será o valor inicial x passado, e os argumentos de todas as subsequentes serão o resultado da anterior. Assim, podemos criar composições de qualquer número de funções.

Ao criar composições funcionais, é muito importante monitorar os tipos de parâmetros de entrada e retornar valores de cada função para que não haja erros inesperados, porque passamos o resultado da função anterior para a próxima.

No entanto, já agora vemos problemas com a aplicação da técnica de composição em nosso código, porque a função:

 const replace = (regexp, replacement, str) => str.replace(regexp, replacement); 

espera aceitar três parâmetros de entrada e enviamos apenas um para compor. Outra técnica de FP, Currying, nos ajudará a resolver esse problema.

Currying é a conversão de uma função de muitos argumentos para uma função de um argumento.

Lembra da nossa função de adição do primeiro artigo?

 const add = (x,y) => x+y; 

Pode ser curry assim:

 const add = x => y => x+y; 

A função pega x e retorna um lambda que espera y e executa a ação.

Benefícios do curry:

  • o código parece melhor;
  • funções com curry estão sempre limpas.

Agora, transformamos nossa função de substituição, para que seja necessário apenas um argumento. Como precisamos da função para substituir caracteres na string por uma expressão regular conhecida anteriormente, podemos criar uma função parcialmente aplicada.

 const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); 

Como você pode ver, corrigimos um dos argumentos com uma constante. Isso se deve ao fato de que o curry é realmente um caso especial de uso parcial.

Um aplicativo parcial está agrupando uma função com um invólucro que aceita menos argumentos do que a própria função; o invólucro deve retornar uma função que aceita o restante dos argumentos.

No nosso caso, criamos a função replaceAllNotWordSymbolsGlobal, que é uma opção de substituição parcialmente aplicada. Ele aceita substituição, armazena-o em um fechamento e espera uma linha de entrada para a qual chamará de substituir, e nós regexp com uma constante.

De volta aos palíndromos. Crie uma composição de funções para o tempo do palíndromo:

 const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); 

e a composição das funções da linha com a qual compararemos o potencial palíndromo:

 const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); 

Agora lembre-se do que dissemos acima:
um exemplo típico de composição está passando uma chamada para uma função como argumento para outra
e escreva:

 const testString = '    ';//          , .. ,    ,  ,   -   ,    const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Aqui temos uma solução funcional e bonita:

 const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; const replaceAllNotWordSymbolsGlobal = replacement => str => replace(allNotWordSymbolsRegexpGlobal(), replacement, str); const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const processFormPalindrom = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsGlobal(''), toLowerCase, ); const testString = '    '; const isPalindrom = isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

No entanto, não queremos fazer curry a cada vez ou criar funções parcialmente aplicadas com as mãos. Claro que não queremos, programadores são pessoas preguiçosas. Portanto, como geralmente acontece no FP, escreveremos mais algumas funções:

 const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } 

A função curry usa uma função para ser curry, a armazena em um fechamento e retorna uma lambda. O Lambda espera o restante dos argumentos da função. Cada vez que um argumento é recebido, ele verifica se todos os argumentos declarados são aceitos. Se aceita, a função é chamada e seu resultado é retornado. Caso contrário, a função será exibida novamente.

Também podemos criar uma função parcialmente aplicada para substituir a expressão regular necessária por uma string vazia:

 const replaceAllNotWordSymbolsToEmpltyGlobal = curry(replace)(allNotWordSymbolsRegexpGlobal(), ''); 

Tudo parece estar bem, mas somos perfeccionistas e não gostamos de muitos colchetes, gostaríamos ainda melhor, então escreveremos outra função ou talvez duas:

 const party = (fn, x) => (...args) => fn(x, ...args); 

Esta é uma implementação de abstração para criar funções parciais aplicadas. Ele pega uma função e o primeiro argumento, retorna um lambda que espera o resto e executa a função.

Agora, reescrevemos parte para que possamos criar uma função parcialmente aplicada de vários argumentos:

 const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); 

Vale a pena notar separadamente que as funções geradas dessa maneira podem ser chamadas com qualquer número de argumentos menor que declarado (comprimento de fn.).

 const sum = (a,b,c,d) => a+b+c+d; const fn = curry(sum); const r1 = fn(1,2,3,4);//,   const r2 = fn(1, 2, 3)(4);//       const r3 = fn(1, 2)(3)(4); const r4 = fn(1)(2)(3)(4); const r5 = fn(1)(2, 3, 4); const r6 = fn(1)(2)(3, 4); const r7 = fn(1, 2)(3, 4); 

Vamos voltar aos nossos palíndromos. Podemos reescrever nosso replaceAllNotWordSymbolsToEmpltyGlobal sem colchetes extras:

 const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); 

Vejamos o código inteiro:

 //    -       const allNotWordSymbolsRegexpGlobal = () => /[\.,\/#!$%\^&\*;:{}=\-_~()?\s]/g; const replace = (regexp, replacement, str) => str.replace(regexp, replacement); const toLowerCase = str => str.toLowerCase(); const stringReverse = str => str.split('').reverse().join(''); const isStringsEqual = (strA, strB) => strA === strB; //       const testString = '    '; //           -    rambda.js const compose = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x); const curry = fn => (...args) => { if (fn.length > args.length) { const f = fn.bind(null, ...args); return curry(f); } else { return fn(...args) } } const party = (fn, ...args) => (...rest) => fn(...args.concat(rest)); //       const replaceAllNotWordSymbolsToEmpltyGlobal = party(replace,allNotWordSymbolsRegexpGlobal(), ''); const processFormPalindrom = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, stringReverse ); const processFormTestString = compose( replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, ); const checkPalindrom = testString => isStringsEqual(processFormPalindrom(testString), processFormTestString(testString)); 

Parece ótimo, mas e se não for uma string para nós, mas houver uma matriz? Portanto, adicionamos mais uma função:

 const map = fn => (...args) => args.map(fn); 

Agora, se tivermos uma matriz para testar palíndromos, então:

 const palindroms = ['    ','   ','   '. ' '] map(checkPalindrom )(...palindroms ); // [true, true, true, false]   

Foi assim que resolvemos a tarefa escrevendo conjuntos de recursos. Preste atenção ao estilo inútil de escrever código - este é um teste decisivo da pureza funcional.

Agora um pouco mais de teoria. Ao usar o curry, não esqueça que, sempre que curry uma função, você cria uma nova, ou seja, selecione uma célula de memória para ela. É importante monitorar isso para evitar vazamentos.

Bibliotecas funcionais como ramda.js têm funções de composição e canal. compose implementa o algoritmo de composição da direita para a esquerda e canaliza da esquerda para a direita. Nossa função de composição é um análogo de pipe da ramda. Existem duas funções de composição diferentes na biblioteca desde composição da direita para a esquerda e esquerda para a direita são dois contratos diferentes de programação funcional. Se um dos leitores encontrar um artigo que descreva todos os contratos existentes do PF, compartilhe-o nos comentários, eu o leio com prazer e acrescentarei um comentário ao comentário!

O número de parâmetros formais de uma função é chamado arity . essa também é uma definição importante do ponto de vista da teoria das transições de fase.

Conclusão


No âmbito deste artigo, examinamos técnicas de programação funcional como composição, currying e aplicação parcial. É claro que, em projetos reais, você usará bibliotecas prontas com essas ferramentas, mas como parte do artigo, implementei tudo em JS nativo para que leitores com talvez pouca experiência no FP possam entender como essas técnicas funcionam sob o capô.

Eu também escolhi deliberadamente o método de narração - pseudo revisão de código, para ilustrar minha lógica de obter pureza funcional no código.

A propósito, você pode continuar o desenvolvimento deste módulo de trabalho com palíndromos e desenvolver suas idéias, por exemplo, baixar linhas por API, converter em conjuntos de letras e enviar para o servidor onde a linha será gerada pelo palíndromo e muito mais ... A seu critério.

Também seria bom se livrar da duplicação nos processos dessas linhas:

  replaceAllNotWordSymbolsToEmpltyGlobal, toLowerCase, 

Em geral, é possível e necessário melhorar o código constantemente!

Até artigos futuros.

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


All Articles