Programação Funcional em JavaScript: Imutabilidade, Pureza e Composição: Do Básico ao Avançado Já leu

Imutabilidade: O Fundamento da Programação Funcional A imutabilidade é o pilar central da programação funcional. Significa que, uma vez criado, um objeto ou dado nunca deve ser modificado. Em vez de alterar dados existentes, criamos novas versões deles. Isso elimina efeitos colaterais inesperados e torna o código mais previsível e testável. No JavaScript, a imutabilidade não é forçada nativamente, mas podemos implementá-la através de boas práticas. Observe: O spread operator ( ) e são aliados poderosos. Para arrays, usamos métodos que retornam novos arrays: Funções Puras: Previsibilidade e Testabilidade Uma função pura é aquela que, dado um mesmo input, sempre produz o mesmo output, sem depender de estado externo ou causar efeitos colaterais. Ela não modifica variáveis globais, não realiza I/O e não altera seus parâmetros. Funções puras são a essência do código funcional. Veja a diferença: Funções puras são imediatamente testáveis: Um exemplo mais prático com arrays: Composição: Construindo Poder com Simplicidade A composição funcional consiste em criar funções

Imutabilidade: O Fundamento da Programação Funcional

A imutabilidade é o pilar central da programação funcional. Significa que, uma vez criado, um objeto ou dado nunca deve ser modificado. Em vez de alterar dados existentes, criamos novas versões deles. Isso elimina efeitos colaterais inesperados e torna o código mais previsível e testável.

No JavaScript, a imutabilidade não é forçada nativamente, mas podemos implementá-la através de boas práticas. Observe:

// ❌ Abordagem imperativa (mutável)
const usuario = { nome: 'João', idade: 25 };
usuario.idade = 26; // Mutação direta

// ✅ Abordagem funcional (imutável)
const usuario = { nome: 'João', idade: 25 };
const usuarioAtualizado = { ...usuario, idade: 26 };
console.log(usuario.idade); // 25 - original intacto
console.log(usuarioAtualizado.idade); // 26 - novo objeto

O spread operator (...) e Object.assign() são aliados poderosos. Para arrays, usamos métodos que retornam novos arrays:

// ❌ Mutação
const numeros = [1, 2, 3];
numeros.push(4); // Modifica original

// ✅ Imutabilidade
const numeros = [1, 2, 3];
const novoArray = [...numeros, 4]; // Cria novo array
const semPrimeiro = numeros.slice(1); // Não altera original

Funções Puras: Previsibilidade e Testabilidade

Uma função pura é aquela que, dado um mesmo input, sempre produz o mesmo output, sem depender de estado externo ou causar efeitos colaterais. Ela não modifica variáveis globais, não realiza I/O e não altera seus parâmetros.

Funções puras são a essência do código funcional. Veja a diferença:

// ❌ Função impura
let contador = 0;
function incrementar(valor) {
  contador += valor; // Depende e modifica estado externo
  return contador;
}

// ✅ Função pura
function somar(a, b) {
  return a + b; // Sem dependências, sem efeitos colaterais
}

console.log(somar(2, 3)); // Sempre 5
console.log(somar(2, 3)); // Sempre 5

Funções puras são imediatamente testáveis:

function aplicarDesconto(preco, percentual) {
  return preco * (1 - percentual / 100);
}

console.log(aplicarDesconto(100, 10)); // 90
console.log(aplicarDesconto(100, 10)); // 90 - determinístico!

// Fácil de testar
const resultado = aplicarDesconto(200, 20);
console.assert(resultado === 160, 'Desconto incorreto');

Um exemplo mais prático com arrays:

// ✅ Função pura que transforma dados
function filtrarAtivos(usuarios) {
  return usuarios.filter(u => u.ativo === true);
}

function extrairNomes(usuarios) {
  return usuarios.map(u => u.nome);
}

const dados = [
  { nome: 'Ana', ativo: true },
  { nome: 'Bruno', ativo: false },
  { nome: 'Carlos', ativo: true }
];

const ativos = filtrarAtivos(dados);
const nomes = extrairNomes(ativos);
console.log(nomes); // ['Ana', 'Carlos']

Composição: Construindo Poder com Simplicidade

A composição funcional consiste em criar funções pequenas e focadas, depois combiná-las para resolver problemas complexos. Cada função faz uma coisa bem feita, e as conectamos como peças de um quebra-cabeça.

Composição Básica

// Funções simples e puras
const multiplicarPor2 = n => n * 2;
const somar5 = n => n + 5;
const converter = n => `Resultado: ${n}`;

// Composição manual (de dentro para fora)
const valor = 10;
const resultado = converter(somar5(multiplicarPor2(valor)));
console.log(resultado); // "Resultado: 25"

Função de Composição (Composer)

Para evitar o aninhamento excessivo, criamos uma função que compõe outras:

// Função auxiliar para compor
const compose = (...funcoes) => valor => 
  funcoes.reduceRight((acc, fn) => fn(acc), valor);

const multiplicarPor2 = n => n * 2;
const somar5 = n => n + 5;
const converter = n => `Resultado: ${n}`;

const pipeline = compose(converter, somar5, multiplicarPor2);
console.log(pipeline(10)); // "Resultado: 25"

Pipe: Composição da Esquerda para Direita

Às vezes é mais intuitivo ler de esquerda para direita:

const pipe = (...funcoes) => valor => 
  funcoes.reduce((acc, fn) => fn(acc), valor);

const multiplicarPor2 = n => n * 2;
const somar5 = n => n + 5;
const converter = n => `Resultado: ${n}`;

const pipeline = pipe(multiplicarPor2, somar5, converter);
console.log(pipeline(10)); // "Resultado: 25"

Exemplo Prático: Processamento de Dados

// Funções simples
const buscarUsuarios = () => [
  { id: 1, nome: 'Ana', idade: 25, ativo: true },
  { id: 2, nome: 'Bruno', idade: 30, ativo: false },
  { id: 3, nome: 'Carlos', idade: 28, ativo: true }
];

const apenasAtivos = usuarios => usuarios.filter(u => u.ativo);
const maioresQue26 = usuarios => usuarios.filter(u => u.idade > 26);
const obterNomes = usuarios => usuarios.map(u => u.nome);

// Composição
const pipe = (...fns) => v => fns.reduce((acc, fn) => fn(acc), v);

const obterNomesValidos = pipe(
  buscarUsuarios,
  apenasAtivos,
  maioresQue26,
  obterNomes
);

console.log(obterNomesValidos()); // ['Carlos']

Conclusão

Três pilares transformam seu código em JavaScript funcional: Imutabilidade garante que dados não sejam alterados inesperadamente, reduzindo bugs. Funções puras tornam o código determinístico e testável, pois sempre produzem o mesmo resultado para o mesmo input. Composição permite construir soluções complexas juntando funções pequenas, focadas e reutilizáveis.

Esses conceitos não são apenas teóricos — melhoram a qualidade real do seu código, facilitam testes automatizados e tornam manutenção mais segura. Comece pequeno: escreva funções puras, evite mutações com spread operators e componha suas soluções. Com prática, a programação funcional se tornará sua segunda natureza.

Referências


Artigos relacionados