Refatorando para JavaScript Funcional: composição de funções (parte 2)

Halan Pinheiro
The Miners
Published in
5 min readMar 7, 2017

--

If you cannot read in Portuguese, try to use the Google Translator. Each code sample here will be written in English. Feel free to translate this article and share.

No parte 1 deste artigo, partimos de um for simples de somatória…

… e refatoramos com um reduce e um pouco de separação de responsabilidades. Ficou assim:

Agora seguimos com nossa refatoração separando ainda mais as responsabilidades, identificando padrões e elaborando códigos genéricos que possam ser reutilizados facilmente. Vamos lá!

Dividindo para conquistar

Podemos melhorar nosso código agora separando em funções diferentes o momento de cálculo de uma linha e o cálculo final:

const sum = (a, b) => a + b

Note que a, b e xs não parecem bons nomes de variável, certo? Enquanto line parece um nome bem adequado e preciso em seu significado. As variáveisa, b e xs são todas de escopos limitadíssimos e suas implementações são um tanto genéricas. São nomes imparciais. Já o argumento line participa de uma implementação bem mais específica, apesar do escopo igualmente limitado, é suposto que line possua quantity e price, e ambos possam ser multiplicados entre si. Portanto, esse é o motivo de nomes genéricos e nomes específicos. Trata-se de níveis de abstração.

Mas, por que precisamos de uma função de soma se já temos o + nativo do JavaScript? Diferente de linguagens como Ruby, no JavaScript o + não equivale a uma função comum (em Ruby + é método), eu não consigo manuseá-la como qualquer outra: enviar como argumento, receber como retorno ou mesmo verificar aridade e outros atributos comuns de uma função. Tampouco posso sobrescrevê-la! Isso acontece porque o + não é uma função, é um operador. Nossa função de soma simplesmente reescreve o operador + como uma função. A vantagem desta abordagem é que agora podemos alimentar o reduce com nossa nova função +.

(curiosidade: em JavaScript podemos verificar a aridade de uma função assim, fn.length, veja mais detalhes no MDN)

map = fn => xs =>

O próximo passo é reescrever o map e o reduce!

— Calma, mas por que diabos vou reescrever uma função nativa (mais uma!)?

— Porque ela não permite composição direta.

Para que o map e o reduce permitam composição direta vou precisar de duas coisas:

  • Acessá-las antes de conhecer o dado que irá ser recebido (pois a composição que irá fornecer os dados).
  • Modificá-las com uso de uma outra função base antes de conhecer o dado que irá ser recebido.

Em outras palavras preciso alimentá-la com seus dois argumentos em dois momentos diferentes.

Nativamente só temos acesso à função map quando já temos um array: [].map, [1, 2, 3, 4].map. Podemos resolver isso utilizando o protótipo do map: Array.prototype.map, mas ainda vamos precisar apontar para esse map a função de mapeamento antes de passar o dado. Portanto: Array.prototype.map.call(dado, fn) não nos serve.

Primeiro preciso desatrela-la do dado, depois preciso escrevê-las em dois passos; dois momentos. No primeiro, ela recebe a função de operação (fn), em seguida recebe o dado. Para isso, escrevo de forma que uma função retorne outra. A sintaxe do => ajuda bastante neste tipo de escrita.

Acho que mostrando o código fica mais fácil de entender o que preciso:

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

const reduce = (fn, ini) => xs => xs.reduce(fn, ini);

A minha função map recebe apenas uma função e retorna uma outra função que exige apenas um argumento, que é o dado que vamos processar. Ou seja: map(totalLines) irá retornar xs => xs.map(totalLines). Assim, posso chamar as duas funções em dois momentos, map(totalLines)(lines):

Ao chamar a última função enviando o xs, o map é processado e retorna seu valor final.

Já a função reduce recebe dois argumentos, a função de processamento e o valor inicial, de retorno ela entrega uma outra função que recebe somente o dado a ser processado. Ao final teremos: reduce(sum, 0)(lines). Nossa intenção aqui é padronizar a entrada de nossas funções para que a entrada de uma seja a saída da outra, por isso é importante termos funções com apenas um único argumento de entrada.

Repare que tenho duas funções exigindo somente um argumento, lines:

map(totalLines)(lines)

reduce(sum, 0)(lines)

Podemos melhorar a leitura dando um nome adequado para map(totalLines) e outro para reduce(sum, 0), dessa forma teremos duas novas funções. Lembrando que cada uma delas tem aridade 1, ou seja, esperam somente um argumento:

const calculateTotals = map(totalLine);

const sumAll = reduce(sum, 0);

Perceba como o dado flui por entre as funções:

O dado flui por cada função. O resultado de uma função passa a ser a entrada da próxima.
Uma peça de LEGO funciona devido a sua parte inferior ser encaixável na sua parte inferior ou ao contrário. Funções capazes de compor precisam ter encaixes similares.

São como peças de LEGO, a entrada de uma se encaixa perfeitamente na saída da outra. Esse é o conceito básico das composições. Como só há possibilidade de retornar um único valor, o encaixe mais simples e viável são entre funções unárias, ou seja, que recebem somente um único argumento. Existem composições mais complexas e específicas envolvendo vários argumentos de entrada, como é o caso das composições de reducers do Redux, por exemplo. Nesse caso, um dos argumentos é repassado inteiro para todas as funções que fazem parte da composição. Mas esse tipo de composição não será o foco aqui.

Lógica genérica e lógica de negócio

A essa altura está muito claro o que é lógica de negócio e o que não é. Vou separar esses dois tipos de código para facilitar ainda mais:

Perceba o nome das funções e argumentos. Enquanto nosso código genérico, de biblioteca é bem genérico, o nosso código específico diz um pouco sobre o contexto onde ele é aplicado. Isso não é sobre programação funcional, esse conceito pode ser utilizado em qualquer paradigma de programação. A separação fica mais evidente quando as responsabilidades estão bem divididas. E, sem dúvida esse é um dos bons motivos para levar muito a sério o princípio de responsabilidade única.

A camada mais genérica do meu código praticamente nunca precisará de manutenção, apenas em caso de bugs. Já a camada de negócio, poderá ser remodelada o quanto quiser. Garantir a camada genérica simples e funcionando bem é um bom caminho para evitar bugs.

Uma coisa que sempre faço em meus códigos é experimentar a sonoridade dele. Leia cada função da nossa camada de negócio em voz alta:

  • total da linha é: dado uma linha, quantidade da linha multiplicado pelo preço da linha.
  • cálculo de totais é um mapeamento dos totais das linhas.
  • soma total é: dado uma linha, a soma de todos os cálculos de totais desta linha.

Viu só?

O que faremos com essas duas camadas de código? Vejamos na última parte deste artigo, quero ainda mostrar finalmente as composições, tubulações e ainda um pouco de curry e o real motivo de termos reescrito (ou remodelado) tantas funções nativas. Vamos nessa?

--

--