Programação funcional em JS
LidoEsta aula explora os princípios da programação funcional em JavaScript, incluindo funções puras, imutabilidade, composição e currying. Com exemplos práticos e exercícios, você aprenderá a escrever código mais previsível, testável e modular.
A programação funcional é um paradigma de programação que trata a computação como a avaliação de funções matemáticas e evita mudanças de estado e dados mutáveis. Em JavaScript, esse paradigma ganhou destaque por permitir um código mais limpo, previsível e fácil de testar. Nesta aula, vamos explorar os quatro pilares fundamentais da programação funcional: funções puras, imutabilidade, composição e currying. Você verá como aplicar esses conceitos no seu dia a dia para escrever um código mais robusto e modular.
Diferente dos paradigmas imperativos, onde o foco está em como executar tarefas passo a passo, a programação funcional se concentra no que deve ser calculado. Isso leva a funções que são independentes do estado externo, o que facilita a depuração e o raciocínio sobre o código. Vamos mergulhar em cada conceito com exemplos práticos e, ao final, você terá exercícios para fixar o aprendizado.
Funções puras
Uma função pura é aquela que, dado o mesmo conjunto de entradas, sempre retorna o mesmo resultado, e não possui efeitos colaterais observáveis. Em outras palavras, ela não modifica variáveis fora do seu escopo, não faz chamadas de I/O (como leitura de arquivos ou requisições HTTP), não altera o estado do sistema e não depende de valores que possam mudar (como variáveis globais). Funções puras são a base da programação funcional porque garantem previsibilidade e facilitam testes.
Por exemplo, considere uma função que calcula o dobro de um número. Ela é pura porque sempre retorna o mesmo valor para a mesma entrada e não altera nada fora dela. Já uma função que incrementa um contador global é impura, pois modifica o estado externo. Vamos ver um exemplo em código:
// Função pura: sempre retorna o mesmo resultado para a mesma entrada
function dobro(x) {
return x * 2;
}
// Função impura: depende de uma variável externa que pode mudar
let contador = 0;
function incrementar() {
contador++;
return contador;
}
console.log(dobro(5)); // 10
console.log(dobro(5)); // 10
console.log(incrementar()); // 1
console.log(incrementar()); // 2Vantagens das funções puras:
- Previsibilidade: o resultado é sempre o mesmo para as mesmas entradas.
- Testabilidade: você não precisa configurar um estado global para testar.
- Paralelismo: funções puras podem ser executadas em paralelo sem risco de corrida.
- Memoização: é possível armazenar resultados para evitar recomputação.
Imutabilidade
Imutabilidade significa que, uma vez criado, um dado não pode ser alterado. Em vez de modificar objetos ou arrays existentes, criamos novos dados a partir dos originais. Isso evita efeitos colaterais e torna o código mais seguro, especialmente em aplicações complexas com muitos componentes interagindo. Em JavaScript, os objetos e arrays são mutáveis por padrão, mas podemos adotar práticas para tratá-los como imutáveis.
Por exemplo, em vez de usar push para adicionar um item a um array, que modifica o array original, usamos o operador spread ou concat para criar um novo array. Da mesma forma, para atualizar um objeto, usamos Object.assign ou o operador spread para criar um novo objeto com as propriedades alteradas. Vamos ver um exemplo:
const numeros = [1, 2, 3];
// Mutável (impuro): modifica o array original
const numerosMutados = numeros;
numerosMutados.push(4);
console.log(numeros); // [1, 2, 3, 4] - o original foi alterado!
// Imutável: cria um novo array sem alterar o original
const numerosImutaveis = [1, 2, 3];
const novosNumeros = [...numerosImutaveis, 4];
console.log(numerosImutaveis); // [1, 2, 3] - original intacto
console.log(novosNumeros); // [1, 2, 3, 4]
// Objetos
const pessoa = { nome: 'Ana', idade: 30 };
const pessoaAtualizada = { ...pessoa, idade: 31 };
console.log(pessoa); // { nome: 'Ana', idade: 30 } - original intacto
console.log(pessoaAtualizada); // { nome: 'Ana', idade: 31 }Para garantir imutabilidade em estruturas mais complexas, bibliotecas como Immutable.js ou Immer podem ser usadas, mas o JavaScript moderno já oferece métodos como map, filter e reduce que retornam novos arrays, incentivando a imutabilidade.
Composição
Composição é a técnica de combinar funções simples para criar funções mais complexas. Em programação funcional, isso é feito passando a saída de uma função como entrada de outra. A composição permite construir pipelines de processamento de dados de forma declarativa e reutilizável. Em JavaScript, podemos usar funções como compose (de bibliotecas como Ramda) ou simplesmente encadear chamadas de funções.
Por exemplo, suponha que temos duas funções: uma que dobra um número e outra que soma 1. Podemos compô-las para criar uma nova função que primeiro dobra e depois soma 1. A composição pode ser feita de forma manual ou com uma função utilitária. Vamos ver:
const dobrar = x => x * 2;
const somar1 = x => x + 1;
// Composição manual: aplicar somar1 após dobrar
const dobrarESomar1 = x => somar1(dobrar(x));
console.log(dobrarESomar1(5)); // 11
// Função compose genérica (da direita para a esquerda)
function compose(...fns) {
return function(x) {
return fns.reduceRight((acc, fn) => fn(acc), x);
};
}
const transformar = compose(somar1, dobrar);
console.log(transformar(5)); // 11
// Função pipe (da esquerda para a direita)
function pipe(...fns) {
return function(x) {
return fns.reduce((acc, fn) => fn(acc), x);
};
}
const transformarPipe = pipe(dobrar, somar1);
console.log(transformarPipe(5)); // 11A composição é poderosa porque permite criar funções pequenas e focadas, que podem ser combinadas de várias maneiras. Isso promove a reutilização e a clareza, já que cada função faz apenas uma coisa.
Currying
Currying é a técnica de transformar uma função que recebe múltiplos argumentos em uma sequência de funções que recebem um único argumento. Por exemplo, em vez de uma função soma(a, b), temos soma(a)(b). O currying facilita a reutilização de funções e a criação de funções parciais, onde alguns argumentos são fixados e outros ficam para serem fornecidos depois.
Em JavaScript, o currying pode ser feito manualmente ou com funções utilitárias. Vamos ver um exemplo de currying manual e como usá-lo:
// Função normal
function soma(a, b) {
return a + b;
}
console.log(soma(2, 3)); // 5
// Versão curried
function somaCurried(a) {
return function(b) {
return a + b;
};
}
const somaCom2 = somaCurried(2);
console.log(somaCom2(3)); // 5
// Com arrow functions
const somaCurriedArrow = a => b => a + b;
const somaCom5 = somaCurriedArrow(5);
console.log(somaCom5(10)); // 15
// Função utilitária para currying de funções com N argumentos
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
} else {
return (...nextArgs) => curried(...args, ...nextArgs);
}
};
}
const soma3 = (a, b, c) => a + b + c;
const soma3Curried = curry(soma3);
console.log(soma3Curried(1)(2)(3)); // 6
console.log(soma3Curried(1, 2)(3)); // 6O currying é útil em cenários como configurar funções de forma parcial, criar funções de alta ordem e melhorar a composição. Por exemplo, podemos usar currying para criar uma função de saudação personalizada:
const saudacao = (tipo) => (nome) => `${tipo}, ${nome}!`;
const ola = saudacao('Olá');
const tchau = saudacao('Tchau');
console.log(ola('Maria')); // Olá, Maria!
console.log(tchau('João')); // Tchau, João!Boas práticas na programação funcional
Ao adotar a programação funcional em JavaScript, algumas boas práticas podem ajudar: prefira funções puras sempre que possível; evite mutação de dados, criando novos objetos/arrays; use métodos como map, filter e reduce em vez de loops imperativos; e quebre funções grandes em funções menores e compostas. Além disso, aproveite o currying para criar funções reutilizáveis e a composição para construir pipelines claros.
É importante lembrar que JavaScript não é uma linguagem puramente funcional, mas podemos aplicar esses princípios para melhorar a qualidade do código. Não é necessário aplicar tudo o tempo todo; use o bom senso e as necessidades do projeto.
Referências
- Função pura - MDN
- Imutável - MDN
- Currying in JavaScript - Eric Elliott
- Compose - Ramda Documentation
- Mostly Adequate Guide to Functional Programming
- Functional Programming in JavaScript - freeCodeCamp
Exercícios
Crie uma função pura chamada
areaRetanguloque recebe base e altura e retorna a área. Teste com dois valores diferentes e verifique se o resultado é sempre o mesmo para as mesmas entradas.✓ Resposta:function areaRetangulo(base, altura) { return base * altura; } console.log(areaRetangulo(2, 3)); // 6 console.log(areaRetangulo(2, 3)); // 6Dado o array
const numeros = [1, 2, 3, 4];, escreva uma expressão que retorne um novo array com todos os números dobrados, sem modificar o original.✓ Resposta:const numeros = [1, 2, 3, 4]; const dobrados = numeros.map(x => x * 2); console.log(dobrados); // [2, 4, 6, 8] console.log(numeros); // [1, 2, 3, 4]Componha as funções
add1emultiply2para criar uma função que primeiro multiplica por 2 e depois soma 1. Use a funçãopipemostrada na aula.✓ Resposta:const add1 = x => x + 1; const multiply2 = x => x * 2; function pipe(...fns) { return function(x) { return fns.reduce((acc, fn) => fn(acc), x); }; } const add1AfterMultiply2 = pipe(multiply2, add1); console.log(add1AfterMultiply2(5)); // 11Implemente uma função
currygenérica e use-a para criar uma versão curried da funçãosubtract(a, b, c)que retorna a - b - c. Teste comsubtractCurried(10)(2)(3).✓ Resposta:function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn(...args); } else { return (...nextArgs) => curried(...args, ...nextArgs); } }; } const subtract = (a, b, c) => a - b - c; const subtractCurried = curry(subtract); console.log(subtractCurried(10)(2)(3)); // 5Crie uma função pura
formatNameque recebe um objeto{ primeiroNome, sobrenome }e retorna a string "Sobrenome, PrimeiroNome". Use imutabilidade para criar uma versão que retorne um novo objeto com uma propriedadenomeCompletoadicionada, sem alterar o original.✓ Resposta:function formatName(pessoa) { return `${pessoa.sobrenome}, ${pessoa.primeiroNome}`; } function addNomeCompleto(pessoa) { return { ...pessoa, nomeCompleto: formatName(pessoa) }; } const pessoa = { primeiroNome: 'Maria', sobrenome: 'Silva' }; const novaPessoa = addNomeCompleto(pessoa); console.log(novaPessoa); // { primeiroNome: 'Maria', sobrenome: 'Silva', nomeCompleto: 'Silva, Maria' } console.log(pessoa); // { primeiroNome: 'Maria', sobrenome: 'Silva' }