Entendendo as funções map, filter e reduce

Photo by ian dooley on Unsplash

Muitas vezes para fazer operações com listas, temos que escrever um código para fazer uma iteração e assim obtermos valores desejados com base na lista principal. Esse conjunto de funções map, filter e reduce nos permite efetuar operações em listas, como transformação, filtragem e agregação de forma muito limpa e prática.

Imagine que precisamos receber uma lista de preços e adicionar 10% em todos os elementos da lista. Numa abordagem tradicional, faríamos algo nesse sentido:

const prices = [50, 60, 70]
const newPrices = []
for (i = 0; i < prices.length; i++) { 
let newPrice = prices[i] * 1.1
newPrices.push(newPrice)
}
// newPrices [55, 66, 77]

Utilizando o método map(), teríamos esse resultado:

const prices = [50, 60, 70]
// Adiciona 10% ao preço
const newPrices = prices.map((price) => price * 1.1)
// newPrices [55, 66, 77]

Prático, não? Utilizando esse grupo de funções, temos muitas vantagens:

  • Melhor legibilidade e manutenibilidade do código
  • Código simples e conciso
  • Imutabilidade. O resultado da nova lista nunca irá alterar os valores da lista original

Para entendê-los, vamos ver um pouco de contexto:

Pure functions

Uma pure function é uma função onde o resultado é apenas determinado pelos seus parâmetros de entrada, sem depender de fatores externos (APIs, sistema de arquivos, qualquer dependência que possa alterar o resultado de uma função). Sendo assim, uma pure function SEMPRE retornará o mesmo resultado para o mesmo parâmetro de entrada.

As funções matemáticas são ótimos exemplos de pure functions. Por exemplo, Math.cos(x) sempre retornará o mesmo cosseno para o parâmetro de entrada x.

Para criar uma pure function, temos duas sintaxes principais no javascript:

// Regular javascript function
function add10Percent(value){
return value * 1.1
}
// OR
// Arrow function
const add10Percent = (value) => value * 1.1
add10Percent(10)
// Returns 10.1

As pure functions são a base para os métodos Map, Function e Reduce. Todas elas recebem uma função como parâmetro, e fazem o processamento baseado nela.

Map

Segundo a documentação:

O método map() invoca a função callback passada por argumento para cada elemento do Array e devolve um novo Array como resultado.

De uma forma mais amigável, o map() executa uma função de transformação em todos os elementos de uma determinada lista , e retorna uma nova lista como resultado.

No nosso exemplo anterior:

[50, 60, 70].map(value => value * 1.1)
// [55, 66, 77]
OU
const add10Percent = (value) => value * 1.1
[50, 60, 70].map(add10Percent)
// [55, 66, 77]

Uma outra utilização muito comum do map() é para fazer mapeamento de contratos de apis. Por exemplo:

[50, 60, 70].map((value) => {price: value} )
// Returns:
// [ { price: 50 }, { price: 60 }, { price: 70 } ]

Filter

Segundo a documentação:

O método filter() cria um novo array com todos os elementos que passaram no teste implementado pela função fornecida.

De forma simples, todos os elementos da lista serão testados em uma pure function que deverá retornar true ou false. Todos os valores que retornaram true serão inseridos na nova lista. Por exemplo:

# Filtrar os preços maiores que 70
const prices = [ 78, 51, 36, 94, 56, 25, 90, 89, 62, 57 ]
prices.filter((value)=> value > 70)]
// [ 78, 94, 90, 89 ]

Reduce

Segundo a documentação:

O método reduce()executa uma função reducer (provida por você) para cada membro do array, resultando num único valor de retorno.

Para simplificar, uma função reducer é uma função agregadora. Pode ser para fazer uma soma, encontrar o maior ou menor valor numa lista, entre muitas outras possibilidades. O reducer sempre percorrerá toda a lista e retornará apenas um resultado.

Neste exemplo, vamos construir um reducer que soma todos os itens de uma lista:

// Somar todos os itens da lista
[0, 1, 2, 3, 4].reduce( (accum, curr) => accum + curr )
// Returns: 10

No reducer, a função é executada 4 vezes e retorna os valores em cada chamada, sendo:

Fonte

Usando reducer com objetos

Vamos utilizar um reducer pra extrair todos os livros do seguinte conjunto de dados:

const data = [  
{
"name":"John",
"books":[
"Harry Potter",
"1984"
]
},
{
"name":"Peter",
"books":[
"Captains of the sands",
"Barren Lives"
]
}
]

Para extrair apenas uma lista de livros de todas as pessoas, poderíamos implementar dessa forma:

/* Estamos concatenando o acumulador com os livros do registro atual em uma única lista*/
data.reduce(
(prev, curr)=> prev.concat(curr.books),
[] // Este é o valor inicial do acumulador (prev)
)
// Returns
[ 'Harry Potter',
'1984',
'Captains of the sands',
'Barren Lives' ]

Outros exemplos

Um uso muito comum dessas funções é usá-las em sequência. Imagine o seguinte cenário:

Temos uma lista de dados não estruturados referentes a quantidade de estoque de um produto, onde cada item da lista é composto por uma string no formato “COD_PRODUTO:ESTOQUE”. Precisamos transformar esses dados para poder utilizá-los e então filtrar apenas estoques maiores que zero:

const products = ["ABC:12", "DEF:77", "GKL:0", "ZZZ:0", "OPQ:10"]
products
.map(p => ({
product: p.split(":")[0],
stock: Number.parseInt(p.split(":")[1])
})).filter(p => p.stock > 0)
// Resultado
[ { product: 'ABC', stock: '12' },
{ product: 'DEF', stock: '77' },
{ product: 'OPQ', stock: '10' } ]

Python, Java e muito mais

Utilizamos javascript para os exemplos de hoje, mas o mesmo conceito se aplica para várias linguagens. Precisamos apenas de uma pure function e uma lista ;)

Python

# uma função para elevar todos os numeros da lista ao quadrado (^2)
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))
# Returns: [1, 4, 9, 16, 25]

Java

String result = map.entrySet().stream()
.filter(x -> "something".equals(x.getValue()))
.map(x->x.getValue())
.collect(Collectors.joining());

Olá, programação funcional

A programação funcional é um paradigma da computação que trata os comandos como funções matemáticas, tendo como um de seus pilares a imutabilidade, que evita alterar diretamente o estado e referências de memória.

Esse paradigma tem crescido bastante nos últimos anos, pois facilita a isolação de problemas em funções puras, garantindo que os resultados produzidos dependerão apenas dos parâmetros passados, evitando efeitos colaterais indesejados.

Como mencionado antes, sempre que executamos essas funções, é retornada uma nova lista, mantendo o objeto original intacto. Isso faz com que as funções Map, Filter e Reduce sejam funções puras e imutáveis, apropriadas para serem utilizadas no paradigma da programação funcional.

Conclusão

Neste artigo vimos alguns usos básicos dos métodos Map, Filter e Reduce. Com eles mantemos nosso código mais simples e conciso, além de nos beneficiarmos dos conceitos da programação funcional, evitando efeitos colaterais indesejados causados pela alteração direta de estado.

Referências