JS funcional # 6: Composição da função

Krzysztof Czernek Segue 26 de jun · 6 min ler

Esta é uma sexta parte da série “Functional JS”. Vá para a parte anterior , ou o começo da série .

Foto de on

Introdução

Até este ponto da série, nos concentramos em técnicas e vocabulário ligados à programação funcional e tentando entender como eles funcionariam em JavaScript.

Ao discutir isso, temos repetidamente aludido à composição de funções : como ter pequenas funções facilita a composição, como o curing afeta o modo como compomos as funções, etc.

Agora, vamos tentar olhar para a imagem completa. Vamos chegar ao fundo da composição da função e como ela pode nos ajudar a escrever um código mais fácil de ler, entender e evoluir.

Composição da função, hein?

Nos termos mais gerais, podemos dividir toda a disciplina de programação em alguns estágios:

  1. compreender lógica de negócios complexa de um problema,
  2. quebrando o problema para um conjunto de problemas menores,
  3. resolvendo os problemas menores, um de cada vez, e
  4. colocando-o de volta para formar uma solução coerente.

O número 4 dessa lista – compondo programas de unidades lógicas menores – é um dos aspectos mais importantes e desafiadores da engenharia de software.

Composição de função é a principal ferramenta que nos ajuda a conseguir isso no estilo funcional da programação.

Formas de compor a funcionalidade

Durante a programação, muitas vezes precisamos executar algumas operações na mesma coisa (seja um objeto, uma coleção, um número etc.), um após o outro. Por exemplo:

  • Nós temos um número x = 7 , e nós queremos adicionar 1 a ele, e depois esquadramos o resultado (e obtemos 64 ), ou
  • Temos uma matriz arr = [1, 2, 3, 4] e queremos filtrar os números ímpares e, em seguida, calcular a soma de todos os números restantes (e obter 6 ).

Para fazer isso, precisamos ser capazes de expressar uma sequência de operações no código.

Em matemática, podemos confiar na precedência do operador e nos parênteses, assim:

 (x + 1) ^ 2 

Em um tipo de programação orientada a objetos, poderíamos encadear chamadas de método:

Quando se trata de programação funcional, normalmente procuramos pequenas funções reutilizáveis. Poderíamos então expressar uma cadeia de operações compondo essas funções, como no exemplo abaixo:

Todos esses exemplos representam maneiras pelas quais podemos expressar a execução de várias operações em uma única entidade.

Composição função

No último trecho de código, vimos um exemplo de composição de função. A maneira como o sumEven funciona é que primeiro chama filterOutOdd em uma coleção, que retorna a coleção despojada de números ímpares. Então nós passamos isso para sum , que retorna uma soma de seus elementos.

Fundamentalmente, podemos dizer que o sumEven usa funções simples para modelar um comportamento mais complexo. Este é um padrão básico de composição de funções que é comumente usado.

Podemos expressá-lo de maneira mais geral como:

Esta funcionalidade de chamar a primeira função e passar seu resultado diretamente para uma segunda função pode ser extraída para evitar a repetição de código. Vamos criar uma função de compose que represente esse conceito:

Vamos agora reescrever a função sumEven acima para fazer uso de compose :

É claro que pode haver mais de duas funções que queremos compor – e é aí que temos o benefício de evitar todos os parênteses aninhados. Vamos escrever uma versão de compose que funcione para três funções:

Podemos generalizar esse conceito para qualquer número de funções:

A encomenda parece … fora?

Se você ler a linha de compose(addOne, double, square)(2) acima e pensar em como ela funciona, você pode ter se encolhido um pouco – a ordem das funções parece estar desativada. As funções são aplicadas da direita para a esquerda: primeiro, 2 é square d, depois é double d e, em seguida, addOne é chamado.

O modo compose é usado reflete a ordem das funções como seria escrita de maneira regular: addOne(double(square(2))) === compose(addOne, double, square)(2) .

Se você acha que seria mais intuitivo ler as funções na ordem em que estão sendo chamadas, então pipe é seu amigo. Ele funciona exatamente como compose , mas aplica as funções da esquerda para a direita.

Bibliotecas para o resgate

Como o FP é uma questão de reutilizar e compor funcionalidades, não é surpresa que a maioria dos utilitários já tenha sido implementada em bibliotecas de código aberto. compose utilitários compose e pipe são um dos blocos de construção mais importantes da programação funcional, portanto, você os encontrará na maioria das bibliotecas FP. Por exemplo:

De volta ao curry!

Há um detalhe que descrevemos com nossos exemplos. Todos eles eram baseados em funções que são unárias (apenas aceitam um argumento): double , addOne , filterOutOdd , etc. Quão conveniente.

O mundo real não é tão bonito assim. Vamos tentar compor funções que aceitem mais de um argumento (como map ):

Se você me perguntar, a parte do map(map(messages, getLength), double) parece difícil de processar. Como podemos torná-lo mais elegante e abstrato longe de todo o assentamento?

Olhando para as definições do pipe e da compose acima, podemos ver que as funções nas quais elas operam devem ser unárias.

Vamos fazer algumas funções unárias!

Este é um truque similar que usamos antes com filterOutOdd .

Agora, podemos usar a técnica da – currying – para evitar a introdução das mapToLengths intermediárias mapToLengths e mapToDoubles . Vamos criar uma versão curry do map :

Como você pode ver, escolhemos map para aceitar fn e collection nessa ordem, para torná-los mais intuitivos na última linha.

Que tipo de funções para compor?

Como pudemos ver, é mais fácil compor funcionalidades com funções que são:

  • unários – aqueles que aceitam apenas um argumento,
  • pequeno – para conseguir mais reutilização,
  • puro – isso é especialmente importante ao compor funcionalidades complexas, já que a impureza da função é "contagiosa",
  • Curry e aceitar dados como o último argumento – para tornar as expressões mais concisas.

O último ponto é uma das opções de design do – a biblioteca de utilitários FP mencionada acima. É por isso que é uma escolha muito popular na comunidade do Functional JS.

Vale a pena?

Você pode estar pensando agora (não pela primeira vez, tenho certeza) se todo esse mumbo-jumbo funcional realmente vale a pena.

Quando se trata de composição de funções, precisamos pensar sobre o que estamos realmente avaliando e quais são as alternativas.

Em face disso, a composição é apenas uma maneira de criar funcionalidades complexas a partir de blocos mais simples. Isso nos ajuda a criar abstrações. Torna possível agrupar as unidades de lógica de aplicação em um todo coerente e manter a complexidade sob controle.

É claro, você precisa considerar se usar ferramentas como pipe ou compose faz sentido caso a caso.

Por exemplo, eu não poderia argumentar que a segunda abordagem funcional é mais legível do que a primeira aqui:

Por outro lado, o estilo funcional parece realmente elegante aqui se você me perguntasse:

Este código é mais curto, lê quase como um simples Inglês, e você não precisa se preocupar em nomear todas essas variáveis intermediárias (algo que retornaremos).

Você tem que fazer seus próprios julgamentos e usar as ferramentas que você conhece, mas não apenas por causa disso.

Já discutimos por que escrever funções pequenas e puras é benéfico. Vimos como as técnicas de FP podem nos ajudar a escrever código mais sustentável. Se você acredita nisso, então o caso de usar compose ou pipe é simples: é a maneira como colocamos pequenos pedaços de funcionalidade juntos para formar uma solução coerente.

Resumo

Agora sabemos o que queremos dizer quando discutimos a composição das funções e como ela é útil. Por isso, adicionamos outra ferramenta à nossa caixa de ferramentas – uma que é simples, mas também poderosa. Eu tenho certeza que quando você começar a usar compose e pipe e se acostumar com eles, você nunca mais vai querer voltar

Na próxima vez, vamos discutir um aspecto adicional que temos aqui – estilo livre de pontos. Até logo!