Continuando nossos artigos sobre Arrays no JS, hoje vamos explorar as possibilidades do reduce, com ele as possibilidades são infinitas, sendo uma das princípais funções para iterar sobre uma coleção utilizadas em linguagens funcionais, e tendo aplicações até mesmo em sistemas onde o processamente de largas quantidades de dados é essêncial. Então para começar a sua jornada em descobrir as diversas possibilidades que ele nos trás, vamos ver 5 dicas do que você pode fazer utilizando ele.
# 01 - implementar qualquer outro método de array
O reduce é tão poderoso que você consegue implementar qualquer um dos métodos de array que nós vimos no nosso artigo anterior a partir dele, por exemplo:
const forEach = (fn, list) => list.reduce((_, x) => void fn(x), null);
const map = (fn, list) => list.reduce((xs, x) => xs.concat([fn(x)]), []);
const filter = (predicate, list) => {
return list.reduce((xs, x) => xs.concat(predicate(x) ? [x] : []), []);
};
const find = (predicate, list) => {
return list.reduce(
(prev, x) => {
if (prev) return prev;
if (predicate(x)) return x;
},
undefined
);
};
const some = (predicate, list) => {
return list.reduce((bool, x) => bool || predicate(x), false);
};
const every = (predicate, list) => {
return list.reduce((bool, x) => bool && predicate(x), true);
};
Isso pode não parecer tão útil no momento, entretanto, essa capacidade do reduce demonstra que, se for uma operação em arrays, provavelmente você consegue implementar ela utilizando o reduce, além de ser útil saber como traduzir os métodos já existem de array para ele para que seja possível otimizar certos algoritmos, sendo exatamente esse o nosso próximo tópico.
# 02 - Otimizando chamadas em sequência para métodos de array
Algo bem comum é ter que misturar várias operações de array para atingir um determinado objetivo, por exemplo, para saber qual o total de visualizações dos artigos de determinada categoria em um blog, seriam necessários 3 métodos de array:
const posts = [
{
category: 'Technology',
title: 'Introduction to JavaScript',
views: 1200,
},
{
category: 'Technology',
title: 'Introduction to PHP',
views: 800,
},
{
category: 'Food',
title: 'Delicious Pasta Recipes',
views: 1500,
},
{
category: 'Food',
title: 'Delicious Hamburger Recipes',
views: 1000,
},
{
category: 'Fashion',
title: 'Latest Trends in Streetwear',
views: 900,
},
];
const technologyViews = posts
.filter((post) => post.category === 'Technology')
.map((post) => post.views)
.reduce((x, y) => x + y, 0);
Só que, apesar de ser uma solução bem elegante, ela é bem ineficiênte em termos de performance, uma vez que o array vai ser iterado 3 vezes, uma para cada método, ou seja, num código sem o uso deles seriam gastos 3 loops ao invés de apenas 1, e é aqui que o reduce pode salvar o dia, fazendo uso do conhecimento do item anterior, podemos combinar diferentes implementações demonstradas anteriormente para formar um reduce só que resolve o problema sem desperdícios:
const techViews = posts.reduce((views, post) => {
return views + (post.category === 'Technology' ? post.views : 0);
}, 0);
console.log(techViews);
Nesse exemplo, usamos a mesma lógica do filter, onde temos uma operação binária (ali o concat, aqui a soma), e o valor que vai ser juntado ao parâmetro acumulador (views) vai ser decidido por um ternário que vai devolver o valor real no caso da condição ser verdadeira, ou um valor que não muda nada no caso de não atingir a condição (antes um array vazio, e para uma soma, o zero).
A lógica do map é utilizada no momento em que a condição for verdadeira para pegar o post e converter ele nas suas views (nesse caso, simplesmente acessando elas).
Podemos ver que ambos seriam equivalentes, pois se extrairmos as callbacks passadas para cada método no exemplo anterior em funções nomeadas, podemos reutiliza-lás na versão otimizada, ex:
const isTech = (post) => post.category === 'Technology';
const toViews = (post) => post.views;
const sum = (x, y) => x + y;
const technologyViews = posts
.filter(isTech)
.map(toViews)
.reduce(sum, 0);
console.log(technologyViews);
const techViews = posts.reduce((views, post) => {
return sum(views, isTech(post) ? toViews(post) : 0);
}, 0);
console.log(techViews);
# 03 - GroupBy
Seguindo a deixa de operações usando arrays que podem ser implementadas usando o reduce, uma muito famosa é o groupBy, onde geramos um objeto com base nos valores de um array, com esses valores sendo agrupados em um array dentro de uma chave desse objeto que representa esse critério, por exemplo, agrupar os números em pares ou ímpares:
// Antes
[1, 2, 3, 4, 5, 6]
// Depois
{
evens: [2, 4, 6],
odds: [1, 3, 5]
}
A implementação desse algoritmo com reduce é até bem simples, começamos com um objeto vazio sendo usado como valor inicial, e vamos adicionando as chaves dinamicamente baseado num critério, ex:
const numbers = [1, 2, 3, 4, 5, 6];
const result = numbers.reduce(
(acc, x) => {
if (x % 2 === 0) {
return {
...acc,
evens: (acc?.evens ?? []).concat([x])
};
}
return {
...acc,
odds: (acc?.odds ?? []).concat([x])
};
},
{}
);
console.log(result);
Isso pode ser melhorado ao convertermos esse algoritmo em uma função para que possamos reutilizar esse reduce para outros tipos de critérios, isso é possível se ao invés de usarmos uma função predicado igual estivemos fazendo até aqui, fizermos ela retornar qual é a chave que o valor pertence ao invés de um valor booleano, ex:
const numbers = [1, 2, 3, 4, 5, 6];
const groupBy = (keyOf, list) => {
return list.reduce(
(group, item) => {
const key = keyOf(item);
return {
...group,
[key]: (group[key] ?? []).concat([item])
};
},
{}
);
};
const result = groupBy((x) => x % 2 === 0 ? 'evens' : 'odds', numbers);
console.log(result);
A mágica aqui está nessa linha:
[key]: (group[key] ?? []).concat([item])
O [key]
faz com que a propriedade seja criada/atualizada dinamicamente com base na chave devolvida pela função critério (keyOf
), o (group[key] ?? [])
garante que vai ser usado o grupo atual onde o valor vai ser inserido, ou se ele não existir ainda, inicializar ele com um array vazio, e por fim o .concat([item])
adiciona ele no grupo.
# 04 - CountBy
Outra função útil que podemos criar a partir do reduce seria uma variação do groupBy apresentado no tópico anterior, só que nesse caso apenas contando as ocorrências de determinado valor em um array. Se você conseguiu acompanhar bem a lógica do groupBy essa daqui vai ser simples:
const numbers = [1, 2, 3, 4, 5, 6];
const countBy = (keyOf, list) => {
return list.reduce(
(count, item) => {
const key = keyOf(item);
return {
...count,
[key]: (count[key] ?? 0) + 1
}
},
{}
);
};
const result = countBy((x) => x % 2 === 0 ? 'evens' : 'odds', numbers);
console.log(result);
De fato, a variação aqui se resume a mudar a operação realizada no momento em que o agrupamento seria realizado, nesse caso, ao invés de inicializar cada grupo com um array vazio, e agrupar usando a operação de concatenação de arrays, nós inicializamos cada grupo usando 0, e usamos a operação de soma para ir incrementando 1 toda vez que um item for identificado como daquele grupo.
# 05 - Compor funções
Em programação funcional, que é de onde todos esses métodos de iteração de array que estamos vendo ao longo desta série tiraram inspiração, uma das técnicas mais utilizadas é a composição de funções, que seria a ideia de pegar um conjunto de funções e combinar elas em uma, assim resolvendo o problema como se fosse uma construção lego, utilizando vários bloquinhos para criar uma construção.
Por exemplo, podemos ter uma função que recebe uma string, converte ela em letras maiúsculas, formata ela como um título do HTML, e mostra ela na tela atualizando o DOM:
const toUpperCase = (str) => str.toUpperCase();
const toH1 = (str) => `<h1>${str}</h1>`;
const render = (str) => {
document.body.innerHTML = str;
};
const renderUppercaseH1 = (str) => render(toH1(toUpperCase(str)));
renderUppercaseh1('Hello World!');
Como o JS não tem um operador que combina as funções como linguagens puramente funcionais possuem, temos que sempre escrever a função que seria o resultado da composição das outras funções na mão, e inclusive controlar como os parâmetros vão ser passados.
Entretanto com o reduce, não precisamos mais desse trabalho todo, podemos compor essas funções usando uma função que faz isso para gente por meio do reduce, ex:
const compose = (...fns) => (arg) => {
return fns.reduce((param, fn) => fn(param), arg);
};
const toUpperCase = (str) => str.toUpperCase();
const toH1 = (str) => `<h1>${str}</h1>`;
const render = (str) => {
console.log(str);
};
const renderUppercaseH1 = compose(toUpperCase, toH1, render);
renderUppercaseH1('Hello World!');
O truque aqui é que fns
é um array graças ao uso do rest operator, e por isso podemos usar o reduce nele, onde cada item desse array vai ser uma função, e elas vão ser executadas em ordem, com cada função produzindo como resultado, um valor que vai ser usado de entrada para a próxima função, com o detalhe de que na primeira execução, o parâmetro que vai ser passado vai ser o parâmetro arg
recebido anteriormente, pois ele vai ser usado como valor inicial do array.
Sendo assim o fluxo de execução seria o seguinte:
compose(toUpperCase, toH1, render); => renderUppercaseH1
renderUppercaseH1("Hello World!")
fns: [toUpperCase, toH1, render], arg: "Hello World!"
No reduce:
param: "Hello World", fn: toUpperCase => "HELLO WORLD!"
param: "HELLO WORLD !", fn: toH1 => "<h1>HELLO WORLD!</h1>"
param: "<h1>HELLO WORLD!</h1>", fn: render => Mostra no console
Dessa forma, o reduce nos ajudou a criar uma função que habilita o uso de outras técnicas do paradigma funcional de forma mais idiomática no nosso código.
Conclusão
Espero que tenha gostado do artigo de hoje, não se esqueça de compartilhar com outras pessoas que ainda não descobriram as maravilhas do reduce caso tenha gostado, e até a próxima.
Ass. Suporte Cansado.
Top comments (0)