9 Soluções Práticas em Programação Funcional para Você Saber (em TypeScript e Node)
Exemplos simples para qualquer desenvolvedor
Esse artigo e uma tradução livre do artigo 9 Useful Functional Programming Solutions You Can Learn (in TypeScript and Node) escrito por Ardy Gallego Dedase.
Como desenvolvedores sempre nos encontramos nas mesma situações: escrevendo ou revisando um trecho de código. E ai, do nada, a nossa intuição nos diz que conseguimos super simplificar nosso código. Ou, se estamos familiarizados com o paradigma de programação imperativa, parece que existe um jeito “funcional” de resolver o problema.
Esse artigo foi escrito para nos desbloquear nesse tipo de situação. Afinal, conseguimos tirar inspiração de qualquer lugar. É apenas uma questão de achar as dicas certas para achar a solução. E quanto mais perto a dica for da solução, melhor.
Passaremos por um conjunto de tarefas de codificação bem conhecidas ordenadas em dificuldade. O autor usou TypeScript com Node 10 para testar cada solução.
1. Teste Unitários Mockados
Problema
Ao rodar os teste localmente, sobre uma função assíncrona, você quer que os dados mockados sejam utilizados.
Solução não funcional
Usar um if-else
para determinar qual função rodar, a assíncrona ou a mockada.
Solução funcional
Atribua as funções assíncrona e mockada à variáveis. Então, use um operador ternário para determinar qual função utilizar com base no ambiente atual.
2. Teste A/B ou Experimentação
Problema
Você quer mostra duas ou mais versões de uma aplicação web: versões A e B.
Solução não funcional
Usar um if-else
para determinar qual versão mostrar.
Solução funcional
Atribua as funções que expõem essas versões à variáveis. Crie um objeto que mapeie as variáveis de função para às versões correspondentes. Use a versão como a chave e a variável de função como valor. Mostre a versão usando o objeto map.
3. Executar uma Série de Funções Sobre um Objeto
Problema
Você tem uma série de funções que pegam um objeto, executam uma série de atualizações nesse objeto e retornam um objeto transformado ou atualizado:
Solução não funcional
Você executa uma função por linha. Por exemplo, inicializa um objeto funcionario
com um id
. E ai uma série de funções atualizarão o objeto funcionario
e retornam um objeto funcionario
atualizado.
O código acima faz todo o trabalho. Você está aplicando cada função em uma linha e mudando o funcionario
. Imagine que você precisa adicionar uma nova função. Você poderia adicionar uma nova linha para chamar a função e passaria o objeto funcionario
como argumento. Nada mal, mas você consegue fazer melhor.
Solução funcional
O jeito funcional é atribuir as funções a um array, iterar sobre ele usando um forEach
e executar cada função sobre o argumento. Note que o argumento funcionario
passado a cada execução é o retorno da função anterior. Você estará usando as funções na mesma ordem que elas foram colocadas no array.
Se você precisar adicionar outra função a ser executada, adicione outra entrada no array funcoesFuncionarioArray
. Você não precisará atribuir novamente o valor retornado para funcionario
e passa-lo como um argumento para a próxima função.
4. Calculadora Simples
Problema
Você quer implementar uma calculadora simples que aceita dois números
Solução não funcional
A solução orientada a objetos é criar uma classe Calculadora
que contenha as operações como métodos, no caso soma
e subtracao
. Se você quiser adicionar mais operações, terá que atualizar a classe para que cada operação tenha um método correspondente. Outro jeito é escrever uma função para cada operação. Por exemplo, implementar as funções calculaSoma()
e calculaDiferenca()
.
Solução funcional
Criar uma função calcular()
que aceita uma função de operação (ex. soma()
) como argumento que será usado para fazer o cálculo desejado. A função que faz o cálculo é também auto-contida e você pode usá-la sem ser passada como um argumento.
5. Calculadora de Conversão
Problema
Converter metros
em uma unidade de medida imperial como milha
, jarda
, pé
e polegada
. As funções de conversão pegam um valor x
em metros
e retornam o valor convertido numa unidade de medida imperial com seu símbolo correspondente.
Solução não funcional
Escrever uma função para cada conversão.
O código duplicado nas funções de conversão é evidente no exemplo acima. Multiplicar e formatar strings é repetido em cada função.
Solução funcional
Tente resolver isso criando uma função que lidará de forma diferente com as conversões por maximizar a reutilização de código entre funções com comportamentos ligeiramente diferentes.
A função criarConversor()
abaixo aceita a o valor de conversão v
e um símbolo s
como argumentos. Ela retorna a função que pega um argumento x
, que é o valor a ser convertido.
const criarConversor = (v: number, s: string) =>
(x: number) => `${x * v} ${s}`
Reutilize a função criarConversor
acima para criar as funções de conversão.
const metrosParaMilhas = criarConversor(0.0006213689, 'mi')
console.log(metrosParaMilhas(1000))
Assim, você vai maximizar o reuso de código bem como sua manutenibilidade.
6. Validar Argumentos de Funções
Problema
Você tem uma função que executa uma operação de subtração, sobre um minuendo
e um subtraendo
, e retorna a diferenca
. O valor retornado só pode ser um número não negativo. Assim, você precisará validar seus argumentos de forma que o minuendo
deve sempre ser maior ou igual subtraendo
: minuendo >= subtraendo
.
const subtracao = (minuendo: number, subtraendo: number): number =>
minuendo - subtraendo
De acordo com Class Ace:
- “
minuendo
— Esse é o maior número, ou o todo, do qual uma parte será retirada.-
subtraendo
— Essa é a parte retirada dominuendo
.-
diferença
— Essa é a parte que sobra da subtração.”
Solução não funcional
Tipicamente, validaríamos os argumentos por adicionar um condicional com um retorno prévio.
O código acima funciona. Porém, a função está fazendo mais de uma coisa. Ao invés de apenas retornar a diferença, ela também valida os argumentos. Isso viola o princípio de responsabilidade única (single-responsibility) do SOLID.
Solução funcional
Primeiro, criamos uma função separada para validar os argumentos.
const diferenca = (minuendo: number, subtraendo: number): number =>
minuendo - subtraendo
Criamos uma função diferencaPositiva()
que chama a função de validação minuendoMaiorQueSubtraendo()
com a função subtracao()
como seu argumento. A função diferencaPositiva()
será chamada para assegurar que os argumentos são válidos antes de executar a subtração.
const diferencaPositiva = minuendoMaiorQueSubtraendo(subtracao)
Cada função está fazendo apenas uma coisa para resolver o problema.
7. Contador de Palavras
Problema
Você tem uma string de palavras ou um poema numa variável. Você quer contar o número de vezes que uma palavra ocorre na string. O problema é análogo a contar o número de ocorrências de um determinado elemento num array.
const poema: string =
`Hold fast to dreams
For if dreams die
Life is a broken-winged bird
That cannot fly
Hold fast to dreams
For when dreams go
Life is a barren field
Frozen with snow`.replace(/(\r\n|\n|\r)/gm, ' ').toLowerCase()
Para simplificar a string, substitua as quebras de linha por espaços e transforme em caixa baixa (lowercase).
Solução não funcional
Usar um hashmap para rastrear o número de ocorrência das palavras.
Solução funcional
Use um reduce
com o hashmap contadorDePalavras
como acumulador e palavra
como um elemento a se processado.
Use o operador spread (…) para extrair a chave e o valor do hashmap contadorDePalavras
.
Ai use um ternário para verificar se a palavra já está no hashmap. Se ela estiver no hashmap, incremente o valor. Do contrário, atribua 1
, que significa que a palavra apareceu pela primeira vez.
8. Achar Anagramas
Problema
“Um anagrama é uma espécie de jogo de palavras criado com a reorganização das letras de uma palavra ou expressão para produzir outras palavras ou expressões, utilizando todas as letras originais exatamente uma vez.” — Wikipedia
Dada uma palavra, você quer encontrar seus anagrama(s) em um dado array de palavras.
Por exemplo, você tem o array de palavras abaixo, com os correspondentes anagramas nos comentários.
const palavras: Array<string> = [
'rat', // tar
'car', // arc
'below', // elbow
'taste', // state
'cried', // cider
'study', // dusty
'thing', // night
'chin', // inch
'grab', // brag
'act', // cat
'robed', // bored
'vase', // save
'glean', // angel
'desserts', // stressed
]
A entrada pode ser um dos possíveis anagramas.
const entrada: string = 'save'
Solução não funcional
Existem muitos exemplos de soluções não funcionais para esse problema na internet, então não vamos falar sobre todas as possibilidades.
Solução funcional
Você pode resolver esse problema com uma combinação das funções built-in filter
e reduce
do JavaScript. Reaproveite a mesma função contadorDePalavrasFuncional
do exemplo #7 que usa reduce
, mas ao invés de contar palavras, agora você contará letras numa palavra.
const contadorDeLetrasFuncional = (palavra: Array<string>) => palavra.reduce((contadorDeLetras: Map<string, number>, letra: string) => ({
...contadorDeLetras,
[letra]: contadorDeLetras[letra] ? contadorDeLetras[letra] + 1 : 1,
}), new Map<string, number>());
Use a função filter
para filtrar as palavras que tem as mesmas letras e tamanho do seu valor entrada
.
const achaAnagramas = (palavra: string, palavras: Array<string>): Array<string> => {
return palavras
.filter(entrada => temMesmoTamanhoELetras(palavra, entrada))
.filter(anagrama => anagrama !== palavra);
}
A solução final será a seguinte:
9. Validação de um Form
Problema
Você tem um form que aceita uma entrada do usuário e você precisa validá-la. Uma validação simples poderia ser que o username tenha pelo menos três caracteres ou que o usuário deve informar uma email válido.
const entradas: Object = {
username: 'ar',
email: 'me@',
}
Solução não funcional
Existem muitos exemplos de soluções não funcionais para esse problema na internet, então não vamos falar sobre todas as possibilidades.
Solução funcional
Comece com um hashmap de critérios a serem aplicados sobre as entradas onde a chave é o campo e o valor é um array de critérios. Por exemplo:
As funções temTamanhoMinimo
e eUmEmailValido
verificam se a entrada e retornam uma mensagem de erro.
Use uma combinação de reduce
e map
para acumular as mensagens de erro retornadas ao testar cada entrada contra os correspondentes critérios de validação. Use filter
para excluir as strings vazias.
Conclusão
Recapitulando:
- Exemplos 1, 2, e 3 são bons exemplos de como tratar funções como dados.
- Exemplo 4 mostra que é possível usar funções como argumento.
- Exemplo 5 mostra que é possível retornar funções.
- Exemplo 6 mostra como pode-se usar funções de ordem superior.
- Exemplos 7–9: usa uma combinação dos conceitos anteriores somados à funções built-in como
map
,reduce
efilter
.
Não digo que vocês sempre devem usar as soluções funcionais delineadas acima quando se depararem com os mesmos problemas. Às vezes é uma questão de preferência ou estilo de codificação. Também é de ajuda considerar outros fatores antes de sair desenvolvendo a solução, como seu repositório de código atual, os membros do seu time e os guidelines de código que seguem.
Espero que sua jornada com programação funcional esteja sendo divertida até agora!
Você encontrará os exemplos de código deste artigo em:
Donativos
Se desejar apoiar voluntariamente meu projeto pessoal de traduzir do inglês artigos que tenham um conteúdo relevante para engenheiros e desenvolvedores que falam português (ou apenas me pagar uma cerveja), faz um pix ai:
Chave Pix: 95303051-e84b-4e0a-bd75-d285420107e4