Refatorando para JavaScript Funcional: Composição de Funções (parte 1)

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.

“Que desgraça! Os senhores escrevem para sapateiros e alfaiates?” — Arthur Schopenhauer sobre filósofos escreverem em seu idioma nativo e não em Latim.

A muito me pedem artigos sobre técnicas com JavaScript, sobretudo explicando a “nova onda” funcional (não tão nova). Tenho feito e escrito muita coisa no Telegram, Facebook, escritórios e em pequenos treinamentos. Agora é hora de registrar algumas técnicas no “papel”.

Tenho aprendido muito sobre programação funcional nos últimos anos, não apenas com JavaScript, mas com Haskell, Elixir e Elm (na verdade, Haskell e Elm foram minhas principais fontes de técnicas desse gênero). Esses últimos estudos me ensinaram que eu passei anos “achando” que sabia alguma coisa sobre programação funcional, assim como muita gente, mas na verdade nem todo mundo tem em mente o que se trata exatamente esse paradigma de programação. Da mesma forma, muitas pessoas utilizam recursos de orientação a objetos a anos sem uma boa noção do que se trata e como utilizar. Usar classes não implica usar corretamente com a proposta adequada para o paradigma, assim como saber os movimentos das peças de um xadrez não nos torna estrategistas ou entendedores das nuances de uma partida.

Como muitos já entenderam, JavaScript é uma linguagem neutra, multi-paradigma e pode ser utilizada de várias formas. Nos últimos anos, vários autores têm reforçado o uso do JavaScript com técnicas de FP (Functional Programming) devido à natureza e ao comportamento de suas funções.

Numa rápida pesquisa sobre o tema, o artigo mais antigo que encontrei falando sobre Javascript Functional foi escrito por Shantanu Bhattacharya, publicado pela IBM em 2006. Desta data para cá, o assunto foi retomando pouco a pouco até seu apogeu nos dias de hoje.

Dá pra dizer que desde o início da linguagem a prática de receber funções como argumento era bem comum. Os famosos callbacks sempre existiram. Posteriormente surgiram as primeiras técnicas de encapsulamento utilizando escopos de funções, usando uma técnica que ficou conhecida como closure. Assim, temos em mãos um dos principais pilares da FP, que é:

funções podem ser recebidas ou retornadas por outras

Chamamos essas funções de High-Order Functions, e a essa característica da linguagem damos o nome de Funções como Cidadão de Primeira Classe. Ainda que não tenhamos naturalmente outros conceitos como imutabilidade na própria linguagem (o const não impõe imutabilidade completa. Além disso nas demais linguagens funcionais imutabilidade é inerente e não opcional), hoje já conseguimos utilizar esse e outros recursos aproveitando-se da resiliência e versatilidade do JavaScript (e de todo um novo ferramental que foi construído nos últimos anos).

Em JavaScript as funções são apenas funções, e ao mesmo tempo são valores! Mesmo as funções pertencentes a um objeto, são apenas funções (… construídas a partir de um protótipo e rodando em um contexto this que aponta para uma instância). Assim, todas as funções poderão servir como entrada ou saída de outras. Essa característica possibilita o uso de uma técnica muito importante que vamos abordar em detalhes neste artigo: composição.

Primeiro passo

Primeiramente vamos simplificar um for tradicional para então separar cada etapa em pequenas funções para então compor novas funções que executem o trabalho que queremos.

Vamos ao código:

const lineItems = [
  { name: 'Carmenere', price: 35, quantity: 2 },
  { name: 'Cabernet', price: 50, quantity: 1 },
  { name: 'Merlot', price: 98, quantity: 10 },
];

const sumTotal = lines => {
  let total = 0;

  for(let i = 0; i < lines.length; i++) {
    total += lines[i].quantity * lines[i].price;
  }

  return total;
}

console.log(sumTotal(lineItems)); // 1100

Evocando um dos princípios mais importantes de todos os paradigmas de desenvolvimento: A Responsabilidade Única, o S do SOLID (o quão SOLID é relevante em FP cabe uma boa discussão em outro artigo). Considero esse principio como sendo quase sinônimo de KISS e DRY. Pois entendo que responsabilidade única é sinônimo de manter as coisas simples e de não se repetir. Vamos utilizar este conceito para quebrar nossa função em pelo menos duas:

  • Soma o total de todas as linhas em um valor final

  • Soma o total apenas de uma linha quantidade valor*

Com a extração do cálculo da linha, chegamos ao seguinte código:

const totalLine = line => line.quantity * line.price;

const sumTotal = lines => {
  let total = 0;

  for(let i = 0; i < lines.length; i++) {
    total += totalLine(lines[i])
  }

  return total;
}

console.log(sumTotal(lineItems));

É difícil encontrar quem nunca ouviu falar sobre SOLID ou SRP, caso seja o seu caso, dá uma olhada nesta palestra do Uncle Bob.

Quanto menos coisas nossas funções fizerem, mais fácil será recombiná-las para construir funções maiores e mais específicas. Por isso o princípio da responsabilidade única deve seguir onipresente. É um ponto de partida comum de quase todos os procedimentos de refatoração.

Reduzindo

Nosso código tem um exemplo clássico que usa um padrão não muito raro, segue o esquema:

  • Inicializa um valor (array vazio, objeto vazio ou zero).

  • Executa um loop percorrendo uma coleção de coisas.

  • Para cada item no loop executa um cálculo partindo do valor inicial e acumula esse valor.

  • Retorna o que foi acumulado.

Mais ou menos assim:

let accumulator = 0;

for(let index = 0; index < collection.length; index++) {
  accumulator = // get current accumulator and collection[index] and do some calculation
}

return accumlator;

Esquema básico de um reducer

Esse padrão é bastante utilizado para somatórias ou acumular coisas. Sempre que encontro uma inicialização de variável com um 0, [] ou {} já procuro algum loop por perto e o que esse loop faz internamente interagindo com esse valor. Uma boa quantidade de programadores JavaScript hoje já reconhece esse padrão quase que instantaneamente e normalmente utiliza 0 reduce. Para entender as vantagens do reduce sobre o for recomendo este excelente artigo. É possível escrever praticamente tudo sem recorrer ao for. Ao invés de usá-lo optamos por funções de mais alto nível para cada padrão de loop que surgir: map, filter, reduce, find… Felizmente temos um reduce implementado nativamente no JavaScript desde 2011. Aqui tem um belo artigo sobre reducers e seu poder. Talvez eu também escreva sobre isso em outra ocasião.

Voltando ao código, ele ficará assim:

const lineItems = [
  { name: 'Carmenere', price: 35, quantity: 2 },
  { name: 'Cabernet', price: 50, quantity: 1 },
  { name: 'Merlot', price: 98, quantity: 10 },
];

const totalLine = line =>
  line.quantity * line.price;

const sumTotal = lines =>
  lines.reduce((total, line) => total += totalLine(line), 0);

console.log(sumTotal(lineItems));

Poderíamos parar por aqui se este artigo não fosse sobre composição de funções. Nosso código já está razoavelmente simples, mas este é apenas o ponto inicial para discutirmos o que precisamos.

Na parte 2 deste artigo iremos quebrar ainda mais esse reduce e torná-lo mais flexível para nossa composição final.

Thanks to Renato Higor Do Nascimento, Pedro Menezes, Igor Marques da Silva, and Lucas Bibiano.

We want to work with you. Check out our "What We Do" section!