Composição de funções com JavaScript

É impressionante como não ter nada a perder faz aumentar sua autoconfiança.

Condenada: Volume 1 – Chuck Palahniuk

Talvez você já tenha lido a frase “dividir para conquistar” ao estudar programação. Funções permitem fazer esse tipo de divisão em um programa. Como consequência, reduzimos a repetição de código e isolamos partes do programa para quem fiquem mais fáceis de serem gerenciadas.

No JavaScript, definimos uma função utilizando a keyword (palavra reservada) function. Podemos passar parâmetros (entradas) para uma função e retornar um valor. Na função abaixo, iremos receber uma string como parâmetro e retornar essa string com todas as letras em maiúsculo.

function converteParaMaiusculo(texto) {
    return String(texto).toUpperCase();
}

Funções podem receber múltiplos parâmetros e nenhum, e pode retornar um valor ou nada. Caso você não utilize a keyword return, por padrão a funciona irá retornar undefined.

Em linguagens de programação funcional, uma função pode ser tratada como outro tipo de dado qualquer. Isto é, podemos armazenar funções em uma variável, dentro de um array (vetor) ou passar como parâmetro de uma função. Sabendo disso, vamos armazenar a função converteParaMaiusculo em uma variável apenas para exemplificar.

function converteParaMaiusculo(texto) {
    return String(texto).toUpperCase();
}

const fn1 = converteParaMaiusculo;

fn1('hello'); // retorna 'HELLO'

Quando temos um () depois de uma função, isso significa que ela será executada e retornará um valor. Caso contrário, será retornada a função sem ser executada. Perceba que ao atribuir a função converteParaMaiusculo a variável fn1 os () não foram utilizados.

O termo composição (composition, em inglês) consiste no processo de utilizar duas ou mais funções de forma combinada. Trata-se de um importante conceito na programação funcional.

Composição de dentro para fora

Como foi dito anteriormente, podemos passar uma função como parâmetro. Nessa abordagem, iremos passar a execução de uma função como parâmetro.

Dando continuidade ao exemplo anterior, suponha que queremos remover os possíveis espaços em branco que possam ter antes ou depois do texto e só depois converter tudo para maiúsculo.

O JavaScript já nos fornece uma função chamada trim para remover esses espaços em branco. Vamos então definir uma função para isso.

function removerEspacosEmBranco(texto) {
    return String(texto).trim();
}

Como podemos combinar essa funções. Normalmente fazemos o seguinte:

let textoTratado = removerEspacosEmBranco( ' hello ' );
textoTratado = converteParaMaiusculo(textoTratado);

Uma possível variação desse código seria:

const textoSemEspacosEmBranco = removerEspacosEmBranco( ' hello  ' );
const textoMaiusculo = converteParaMaiusculo(textoSemEspacosEmBranco);

Analisando os códigos, podemos perceber que o resultado da função removerEspacosEmBranco é a entrada da função converteParaMaiusculo e com o auxílio de variáveis estamos conectando essas duas funções.

Ambos os código chegam no mesmo resultado e não existe nenhum problema em deixá-los dessa forma. Trata-se apenas de uma abordagem para resolver um problema. Entretanto podemos combinar essas duas funções para gerar uma outra função.

Para isso, vamos passar a execução da função removerEspacosEmBranco como parâmetro da função converteParaMaiusculo. Assim, o resultado da função removerEspacosEmBranco será a entrada da função converteParaMaiusculo.

const textoTratado = converteParaMaiusculo( 
                        removerEspacosEmBranco ( ' hello  ') 
                    );
// o valor armazenado na variável é 'HELLO'

Primeiro é executado a função removerEspacosEmBranco com a entrada ' hello ' e depois a função converteParaMaiusculo com a entrada 'hello'. Assim, o valor retornado é ‘HELLO’.

Uma coisa que pode parecer estranha é o fluxo de dados que ocorre da direita para a esquerda. Isso porque a função é lida da função mais interna para a mais externa.

textoTratado <-- converteParaMaiusculo <-- removerEspacosEmBranco <-- 'hello'

Composição utilizando uma função

Vamos olhar novamente a composição que fizemos:

converteParaMaiusculo( removerEspacosEmBranco ( ' hello  ' ) );

Para facilitar a reutilização dessas funções combinadas, vamos definir uma função que retorna essa combinação.

function converteParaMaiusculoERemoveEspacosEmBranco( texto ) {
    return converteParaMaiusculo( 
                removerEspacosEmBranco ( texto ) 
           );
}

converteParaMaiusculoERemoveEspacosEmBranco( ' hello  ' );
// retorna 'HELLO'

O que fizemos aqui foi criar uma função que retorna uma função, que é a combinação de duas outras funções. Hmm… provavelmente essa explicação ficou confusa. Mas pense em como poderíamos criar uma função que fosse uma “fábrica de funções”.

Vamos pensar: qual seria a entrada dessa função? Bom, queremos combinar duas funções quaisquer. Então, essas duas funções quaisquer serão a entrada.

function compor2(fn2, fn1) {
    ...
}

Nossa “fábrica” vai produzir uma nova função, então o retorno será uma função.

function compor2(fn2, fn1) {
    return function funcaoComposta (entrada) {
        ...
    }
}

Por fim, temos que retornar as funções combinadas.

function compor2(fn2, fn1) {
    return function funcaoComposta (entrada) {
        return fn2( fn1 ( entrada ) )
    }
}

// resultado <-- fn2 <-- fn1 <-- entrada

Com a função compor2, podemos combinar quaisquer duas funções. Com isso, podemos refatorar o código que combina as funções removerEspacosEmBranco e converteParaMaiusculo. Lembre-se que o fluxo de dados continua sendo da direita para a esquerda.

const converteParaMaiusculoERemoveEspacosEmBranco = compor2(
    converteParaMaiusculo,
    removerEspacosEmBranco
)

converteParaMaiusculoERemoveEspacosEmBranco( ' hello  ' );
// retorna 'HELLO'

A função compor2 já é bastante útil, mas precisamos continuar e definir uma função que nos ajude na composição de um número qualquer de funções. Para isso vamos definir a função compose.

function compose(...fns) {
    return function composed(result) {
        var list = [...fns];

        while(list.length > 0) {
            result = list.pop()(result);
        }

        return result;
    }
}

// resultado <-- fnN <-- ... <-- fn2 <-- fn1 <-- entrada

Não se preocupe em entender o funcionamento interno dessa função. O importante é saber que essa função permite fazer a composição de um número n de funções.

compose(
    converteParaMaiusculo,
    removerCaracteresEspeciais,   // não implementado
    removerNumeros,               // não implementado
    removerEspacosEmBranco
)

Olhando para a composição acima, você consegue saber o que ocorre se a entrada for ' hello123* '?

Poderíamos ler essa composição da seguinte forma:

  • primeiro será removido os espaços em branco;
  • depois será removido os caracteres numéricos;
  • depois será removido os caracteres especiais; e
  • por fim, os caracteres serão convertidos para maiúsculo.

O esforço para ler um código dessa forma tende a ser menor pois conseguimos visualizar o fluxo que o dado vai percorrer. Outro benefício é que podemos passar novas funções como parâmetro da função compose e realizar outras operações sobre a entrada. Além de removê-las sem nenhum problema se for necessário.

Preparando uma função para composição

Para que uma função esteja preparada para ser composta com outras ela precisa receber apenas um parâmetro como entrada. Perceba que as funções implementadas até aqui utilizam apenas um parâmetro.

Caso você tenha uma função com mais de um parâmetro, considere utilizar um objeto.

function formatarData1(data, formato) {
	...
}
formatarData1( '01011900', 'DD/MM/YYYY');

// Passando os parâmetros dentro de um objeto
function formatarData2( { data, formato } ) {
	...
}

formatarData2({ data: '01011900', formato: 'DD/MM/YYYY' });

Preparando você para composição

Linguagens de programação funcional normalmente não são utilizadas no ensino introdutório de programação e sinceramente não tenho certeza se é a melhor escolha para quem está iniciando. Assim, é provável que você esteja acostumado e utilize o paradigma estruturado e/ou orientado a objetos.

Ao utilizar composição, você está dando um passo em direção ao paradigma funcional. O que significa que você terá que fazer um esforço deliberado para programar nesse novo paradigma. Já que se deixar no automático, você continuará programando no que você está mais acostumado.

Nas primeiras tentativas é comum ficar paralisado e não conseguir fazer nada. Entretanto não desanime. Leia novamente algum material sobre composição e veja os exemplos e como você pode adaptá-los no seu código.

Bibliotecas para programação funcional

Nesse post, criamos apenas duas funções para nos auxiliar na composição de funções: compor2 e compose. Entretanto existem bibliotecas que podemos usar e, com isso, evitar de ter que reinventar a roda. Duas dessas bibliotecas são Ramda e lodash/fp.

Ambas possuem o método compose. Além disso, se você não está confortável em utilizar o fluxo de dados da direta para a esquerda, existe também o método pipe que utiliza o fluxo da esquerda para a direita.

Olhando a documentação dessas bibliotecas, você verá que existem diversas funções que já estão preparadas para composição. Como, por exemplo:

  • lodash/fp
    • trim – retorna a string sem espaços em branco;
    • reverse – retorna um array na ordem invertida.
  • Ramda
    • isEmpty – retorna verdadeiro se for vazio;
    • isNil – retorna verdadeiro se for null ou undefined;
    • reverse – retorna uma lista ou string na ordem invertida;
    • dec – decrementa o número passado como entrada.

Leituras recomendadas