Programação Funcional em JavaScript: da teoria à prática no Node.js
Programação Funcional (PF) é um paradigma que considera a computação como a avaliação de funções matemáticas e evita a mudança de estado e dados mutáveis. No contexto de JavaScript e Node.js, a PF permite escrever código mais previsível, testável e de fácil manutenção, especialmente ao trabalhar com fluxos assíncronos e transformações complexas de dados. Este artigo é destinado a desenvolvedores já familiarizados com os fundamentos da PF e se aprofunda em conceitos avançados: composição de funções, functores, mônadas e gerenciamento de efeitos colaterais.
1. Funções Puras e Imutabilidade: a base da confiabilidade
Em um nível avançado, não basta apenas saber a definição de uma função pura. É importante entender como a imutabilidade e a ausência de efeitos colaterais afetam a arquitetura da aplicação, especialmente no ambiente Node.js, onde operações concorrentes são frequentemente utilizadas.
1.1. Cópia profunda e Compartilhamento Estrutural (Structural Sharing)
A cópia profunda completa de objetos grandes pode ser cara. Em vez disso, bibliotecas como Immer ou Immutable.js usam compartilhamento estrutural: ao modificar uma parte do objeto, uma nova referência é criada, mas as partes não modificadas são compartilhadas com o estado anterior.
// Exemplo usando Immer (biblioteca popular)const { produce } = require('immer');
const baseState = { user: { name: 'Alice', age: 30 }, meta: { created: Date.now() }};
// produce cria um novo estado, sem mutar baseStateconst nextState = produce(baseState, draft => { draft.user.age = 31;});
console.log(baseState.user.age); // 30console.log(nextState.user.age); // 31console.log(baseState.meta === nextState.meta); // true (Structural Sharing)1.2. Funções Puras e Memoização
Funções puras são ideais para memoização — armazenamento em cache de resultados para evitar recálculos. No Node.js, isso é crítico para cálculos pesados ou consultas ao banco de dados.
// Memoização simples para uma função puraconst memoize = (fn) => { const cache = new Map(); return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { console.log('Do cache'); return cache.get(key); } const result = fn(...args); cache.set(key, result); return result; };};
const fibonacci = (n) => (n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2));const memoizedFib = memoize(fibonacci);
console.log(memoizedFib(40)); // Calculadoconsole.log(memoizedFib(40)); // Do cache2. Composição de Funções: construindo o complexo a partir do simples
Composição é o processo de combinar duas ou mais funções para obter uma nova função. Em um nível avançado, usamos não apenas a composição simples, mas também padrões como Pipe e Compose, que são a base para a construção de pipelines de processamento de dados.
2.1. Pipe e Compose
Pipe executa funções da esquerda para a direita, enquanto Compose executa da direita para a esquerda. No Node.js, isso é frequentemente usado para processar dados de entrada (por exemplo, requisições HTTP).
// Implementação de pipeconst pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);
// Exemplo: processamento de stringconst toLowerCase = (str) => str.toLowerCase();const removeSpaces = (str) => str.replace(/\\s+/g, '-');const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
const slugify = pipe(toLowerCase, removeSpaces, capitalize);console.log(slugify('Hello World From Node.js')); // Hello-world-from-node.js2.2. Pipelines Assíncronos
Em aplicações Node.js reais, as funções são frequentemente assíncronas. Vamos criar uma versão de pipe para trabalhar com promessas.
const asyncPipe = (...fns) => (x) => fns.reduce((acc, fn) => acc.then(fn), Pr
```