Arrays JavaScript são uma forma incrivelmente flexível de modelar coleções usando técnicas de programação funcional. Este artigo apresenta o uso de ferramentas como forEach()
, map()
e reduce()
para matrizes de estilo funcional.
Matrizes JavaScript tradicionais
Os arrays do JavaScript podem conter tipos heterogêneos, alterar o tamanho rapidamente e inserir ou remover elementos prontamente. Métodos tradicionais como slice, splice e push/pop fazem isso operando no próprio array, modificando a coleção de forma “destrutiva”:
// Create an array with heterogeneous types:
let myArray = (10, "hello", true, { name: "Alice" });
// Add to an array and change size on the fly:
myArray.push(42);
// Extract elements without modifying the original array:
let extracted = myArray.slice(1, 3);
Programação funcional com arrays
Embora os arrays JavaScript sejam muito eficientes desde o início, o paradigma funcional melhora a clareza e a capacidade de manutenção do código do array. Em geral, a programação funcional procura usar funções como operadores que podem ser passados para arrays. Isso permite operar no array como o cabeçote de uma fita, em vez dos tradicionais loops imperativos que descrevem em detalhes o que deve ocorrer.
Vejamos alguns exemplos de trabalho com arrays no paradigma funcional.
para cada()
Array.forEach()
é nosso primeiro exemplo. Isso permite passar uma função que executa operações arbitrárias nos elementos iterativamente. É uma alternativa comum ao tradicional for
laço:
myArray.forEach((element) => {
console.log("my element is: " + element);
})
Neste exemplo, estamos apenas enviando cada elemento para o console. O equivalente em um for
circuito seria:
for (let i = 0; i < myArray.length; i++) {
console.log("my element is: " + myArray(i));
}
Você notará que há menos peças móveis na versão funcional. Em particular, eliminamos o iterador (i
), que é uma variável estranha usada para expressar a lógica da mecânica, e não uma parte da intenção real. Observe que não estou sugerindo for
os loops não têm lugar; às vezes são a ferramenta certa, mas muitas vezes, forEach()
é uma abordagem mais limpa.
A programação funcional como filosofia promove a “imutabilidade”. Isso significa simplesmente que ele gosta de evitar a modificação de variáveis. Em vez disso, a programação funcional prefere pegar uma variável existente e passá-la por um “pipeline” (uma função ou funções) que a transforma em uma nova variável, deixando o original como está.
forEach
é frequentemente usado dessa forma, mas também é frequentemente usado “destrutivamente”, como mostrado aqui:
const numbers = (1, 2, 3, 4, 5);
numbers.forEach(function(number, index) {
if (number % 2 === 0) { // Check for even numbers
numbers.splice(index, 1); // Remove even numbers from the array
}
});
Este exemplo pode não ser considerado a forma mais pura de programação funcional, mas utiliza características funcionais importantes, como “funções de primeira ordem”. Quando nos referimos a uma função de primeira ordem, queremos dizer que estamos a utilizar uma função como qualquer outra referência, neste caso, passando-a como argumento. O resumo dessa história é que as funções podem atuar como pacotes portáteis de funcionalidades que são repassadas para realizar trabalhos de maneiras previsíveis.
Note-se, também, que ainda existem muitos casos em que uma abordagem antiquada for
loop é a melhor abordagem. Por exemplo, ao iterar por um número diferente de 1, ao iterar de trás para frente e ao lidar com cenários complexos que exigem vários iteradores.
Array.map()
Funções que não são destrutivas e evitam quaisquer outros “efeitos colaterais” são consideradas “funções puras”. Podemos usar forEach
desta forma, mas o Array.map()
função foi projetada especificamente para esse propósito. Ele não opera no array em si, mas executa o operador de função e retorna o resultado como um novo array:
const bands = (
{ name: "Led Zeppelin", year: 1968 },
{ name: "Pink Floyd", year: 1965 },
{ name: "Queen", year: 1970 },
{ name: "The Clash", year: 1976 },
{ name: "The Ramones", year: 1974 },
{ name: "R.E.M.", year: 1980 },
);
const bandNames = bands.map(band => {
return band.name;
});
// bandNames is an array that has just the string band names
Array.map()
é um mecanismo muito poderoso para transformar matrizes. Dá a você a capacidade de fazer quase tudo com um array de maneira limpa. Em particular, evita a complexidade na alteração do array original, onde outro código em outros lugares pode depender dele de maneiras desconhecidas ou inesperadas.
Por outro lado, é importante ter em mente que Array.map()
sempre faz uma cópia, o que tem implicações no desempenho. Você não deseja usá-lo em matrizes muito grandes. Às vezes, considerações de memória determinam que você use outra abordagem.
Como isso funciona é que tudo o que a função fornecida retornar será mantido no novo array. Portanto, poderíamos usar a versão de retorno automático de uma função:
const bandNames = bands.map(band => band.name)
Essa abordagem pode ser muito mais limpa para funções curtas.
Matriz.filtro()
Array.map()
gera uma matriz com o mesmo comprimento da fonte. Se a função não retornar algo, o array de saída será rotulado undefined
nessa posição. Para criar um array com comprimento diferente, você pode usar Array.filter()
. Nesse caso, quando o argumento funcional não retornar nada, esse elemento será removido do array alvo:
const bands = (
{ name: "Led Zeppelin", year: 1968 },
{ name: "Pink Floyd", year: 1965 },
{ name: "Queen", year: 1970 },
{ name: "The Clash", year: 1976 },
{ name: "The Ramones", year: 1974 },
{ name: "R.E.M.", year: 1980 },
);
const seventiesBands = bands.filter(band => {
if (band.year >= 1970 && band.year < 1980) {
return band;
}
});
// seventiesBands is an array holding only those bands satisfying the condition (band.year >= 1970 && band.year < 1980)
Neste exemplo, pegamos uma série de objetos contendo bandas de rock e o ano em que foram formadas e depois usamos bands.filter()
para fornecer uma função que nos dará uma nova matriz contendo apenas as bandas da década de 1970.
Array.reduce()
Às vezes, você precisa pegar um array inteiro e transformá-lo em um único valor. Para isso, você pode usar Array.reduce
:
// same band array as source
const earliestBand = bands.reduce((earliestSoFar, band) => {
return band.year < earliestSoFar.year ? band : earliestSoFar;
}, { year: Infinity }); // Start with a band in the infinitely distant future
console.log(earliestBand.name); // outputs “Pink Floyd”
A função passada para reduce()
tem dois argumentos: o “acumulador” e o elemento atual. O acumulador é o que será finalmente retornado e mantém seu estado em cada iteração, permitindo “coletar” tudo em uma única saída.
O reduce
function é uma ferramenta muito útil quando você precisa dela. Como outro exemplo rápido, digamos que você queira uma string contendo todos os nomes de bandas em uma string. Você poderia fazer isso:
const allBandNames = bands.reduce((accumulator, band) => {
return accumulator + band.name + ", ";
}, ""); // Initial value is an empty string
Compondo funções
As funções integradas que você viu até agora são fundamentais para a programação funcional (e sua irmã de programação, a programação reativa). Agora, vamos considerar a ideia de vincular funções para obter alguma funcionalidade desejada.
Duas das funções de ligação mais básicas e importantes são compose()
e chain()
. Muitas bibliotecas funcionais e de utilitários os incluem, mas também são fáceis de implementar. O próximo exemplo dá uma visão clara de como eles funcionam:
const compose = (...fns) => (x) => fns.reduceRight((v, f) => f(v), x);
const chain = (...fns) => (xs) => xs.reduce((acc, x) => acc.concat(fns.reduceRight((v, f) => f(v), x)), ());
compose()
combina muitas funções, de modo que a saída de cada função seja alimentada na próxima, da direita para a esquerda (com base na ordem passada para a função). chain()
faz a mesma coisa, mas da esquerda para a direita.
Essas funções também fornecem uma visão reduceRight()
a imagem espelhada de reduce()
, que você já viu. O reduceRight()
A função permite acumular retrocedendo nos argumentos funcionais.
O compose()
e chain()
funções não são específicas para arrays, mas podem ser usadas com eles. Aqui está um exemplo simples de uso compose()
com uma matriz:
const numbers = (1, 4, 2, 8, 5, 7);
// Define reusable higher-order functions:
const findEvenNumbers = arr => arr.filter(n => n % 2 === 0);
const doubleNumbers = arr => arr.map(n => n * 2);
const sortNumbers = arr => arr.sort((a, b) => a - b);
// Compose functions to create complex transformations:
const processNumbers = compose(sortNumbers, doubleNumbers, findEvenNumbers);
const processedNumbers = processNumbers(numbers);
console.log(processedNumbers); // Output: (4, 8, 16)
Conclusão
Organizar funções é fundamental para a programação funcional e reativa. Ele permite reutilizar e combinar funções em novas funções. Em essência, você pode definir funções compostas que são compostas pelos recursos de outras funções mais focadas. Isso é semelhante, conceitualmente, ao modo como um programador orientado a objetos pensa sobre a composição de aplicações a partir de objetos.
Como as funções expressam seu trabalho de maneira minimalista — com entrada e saída sendo toda a superfície da API — elas fornecem uma abordagem excepcionalmente limpa. É claro que, à medida que você se torna mais sofisticado, perde um pouco dessa clareza. Mesmo o compose()
e chain()
funções deixam para trás um pouco da elegância das funções simples.
Em geral, lidar com funções de array como vimos aqui, usando funções integradas do JavaScript como map()
e filter()
é uma excelente aplicação do poder da programação funcional.