Somando valores sem laços
O Zan Franceschi publicou o seguinte desafio:
Como calculas a soma das compras sem usar laços? Map, filter, reduce e recursão são permitidos.
Analisando os tipos
Bem, logo de começo, percebi que temos um objeto bem formado. Portanto, ele tem um tipo muito bem definido e reproduzível. Vamos pegar o json original e destrinchar?
{
"compras": [
{
"data": "2022-01-01",
"produtos": [
{
"cod": "a",
"qtd": 2,
"valor_unitario": 12.24
},
{
"cod": "b",
"qtd": 1,
"valor_unitario": 3.99
},
{
"cod": "c",
"qtd": 3,
"valor_unitario": 98.14
}
]
},
{
"data": "2022-01-02",
"produtos": [
{
"cod": "a",
"qtd": 6,
"valor_unitario": 12.34
},
{
"cod": "b",
"qtd": 1,
"valor_unitario": 3.99
},
{
"cod": "c",
"qtd": 1,
"valor_unitario": 34.02
}
]
}
]
}
Na raiz, temos um objeto que tem um único campo chamado compras.
Vou chamar esse objeto com o nome absurdamente criativo compras.
Dentro desse campo, eu tenho um array de objetos do tipo compra.
Ok, agora, o que é um objeto do tipo compra? Ele é composto por
dois campos:
- a data da compra,
data - um array de produtos comprados naquela compra,
produtos
O campo data é do tipo de data, já o campo produtos é um array
de objetos do tipo produto. E, por sua vez, o que é um produto?
- tem um código de produto, tipo string,
cod - a quantidade comprada, tipo número,
qtd - o valor unitário de cada unidade comprada, tipo número,
valor_unitario
Em uma notação free-style:
compras:
- compras: compra[]
compra:
- data: data
- produtos: produto[]
produto:
- cod: string
- qtd: número
- valor_unitario: número
E como conseguir a soma de todas as compras? Bem, para isso precisamos chegar até produto
e achar o valor da compra do produto, para então achar o valor da compra, para então
achar a soma do vetor de compras.
Navegando os tipos
Eu sei transformar um produto em um valor. Se eu comprei 2 Coca-Colas 2L por 12.24
unitariamente, então o valor total desse produto é valor unitário vezes quantidade,
portanto 24.48.
Daí, consigo fazer o mapeamento produto => valor assim:
function produto2valor(p: Produto): number {
return p.qtd * p.valor_unitario;
}
Mas eu não começo com produto. Eu começo com compras. Como chegar lá? Bem, a resposta
é simples: mapeando. Resgatando os tipos novamente pegando apenas o que nos interessa
(até chegar em produto que produto eu sei trabalhar), temos o seguinte:
compras:
- compras: compra[]
compra:
- produtos: produto[]
Ou seja, de compras consigo acessar um array de compra, e de elementos de compra posso
pegar um mapeamento planificado para o campo produtos, o que me retorna um array de
produto com todos os elementos individuais de trabalho. A diferença entre um mapeamento
clássico e um mapeamento “aplainado” é que no mapeamento clássico eu obtenho o array para
trabalhar com cada array individualmente; já no aplainado eu obtenho os elementos individuais
desse array para trabalhar.
compra.map(c => c.produtos) // cada elemento é do tipo produto[]
compra.flatMap(c => c.produtos) // cada elemento é do tipo produto
Então, agora, se eu souber reduzir uma compra a um valor, posso pegar todas essas reduções e reduzir em uma soma, confere?
function compra2valor(compra: Compra): number {
return // algum valor
}
compras.compras
.map(compra2valor) // agora só somar, trabalhando com tipo number
Confere. A redução de soma é basicamente a seguinte:
- se começa com o elemento neutro da soma
0 - o acumulador é um número
- cada elemento recebido é um número
- a função é simplesmente a soma do acumulador com o atual
acc + current
function compra2valor(compra: Compra): number {
return // algum valor
}
compras.compras
.map(compra2valor)
.reduce((acc, curr) => acc + curr, 0)
Basicamente é isso, agora é só preencher a função mágica compra2valor.
Já sabemos usar produto2valor, então bastaria que eu transformasse
Compra =[]> Produto do mesmo jeito que transformei Compras =[]> Compra,
que então eu aproveitaria o fato de que já tenho Produto => valor. E, bem,
temos aqui a possibilidade de fazer um mapeamento aplainado para produtos:
function compra2valor(compra: Compra): number {
return compra.flatMap(c => c.produtos) // aqui trabalhando com elementos do tipo Produto
}
Que por sua vez posso mapear com produto2valor e reduzir com a soma:
function compra2valor(compra: Compra): number {
return compra.flatMap(c => c.produtos) // aqui trabalhando com elementos do tipo Produto
.map(produto2valor) // aqui cada elemento é um número
.reduce((acc, curr) => acc + curr, 0) // somei todos os números
}
Portanto, o todo seria:
function produto2valor(p: Produto): number {
return p.qtd * p.valor_unitario;
}
function compra2valor(compra: Compra): number {
return compra.flatMap(c => c.produtos)
.map(produto2valor)
.reduce((acc, curr) => acc + curr, 0)
}
compras.compras
.map(compra2valor)
.reduce((acc, curr) => acc + curr, 0)
E se quiser fazer one-liner:
compras.compras
.map(c => c.flatMap(c => c.produtos)
.map(p => p.qtd * p.valor_unitario)
.reduce((acc, curr) => acc + curr, 0))
.reduce((acc, curr) => acc + curr, 0)
Usando propriedades da soma
Eu particularmente achei essa versão one-liner feia. Preciso repetir a mesma operação de soma, não ficou visualmente agradável. Será que tem algo que eu possa fazer para deixar mais elegante?
A resposta é? Sim, claro que há. Vamos rapidinho retornar aqui a como se pega o valor de uma compra:
function compra2valor(compra: Compra): number {
return compra.flatMap(c => c.produtos)
.map(produto2valor)
.reduce((acc, curr) => acc + curr, 0)
}
Note que, aqui, independente do objeto compra passado, se por acaso
tiverem o mesmo array de produtos, então a soma é exatamente a mesma.
De modo geral, as propriedade de compra não importam para o valor
da compra, apenas as propriedade de cada produto. Demais, como a
soma é assossiativa e comutativa:
- por ser assossiativa,
a + (b + c) = (a + b) + c - por ser comutativa,
a + b = b + a
será que posso usar essas propriedades para pegar algo interessante?
Vamos supor que eu tenho 2 compras, as compras a e b. Cada compra
tem um total de item, e a compra a tem 3 itens, cujos valores vão
ser representados por a1, a2 e a3. De modo semelhante tenho b,
com 2 itens. Se eu for inicialmente somar os valores das compras
individualmente para depois somar os valores das compras entre si,
teria a seguinte soma:
((a1 + a2) + a3) +
(b1 + b2)
O que é a mesma coisa disto:
let a12 = a1 + a2
let b12 = b1 + b2
(a12 + a3) + b12
Pela assossiação, tenho que isso é equivalente a
let a12 = a1 + a2
let b12 = b1 + b2
(a12 + a3) + b12
a12 + (a3 + b12) // a partir daqui toda linha é equivalente à de cima
(a1 + a2) + (a3 + b12)
a1 + (a2 + (a3 + b12))
a1 + (a2 + (a3 + (b1 + b2)))
E como também é comutativa, não importa se primeiro eu somo b1 + b2 ou se faço
a1 + b1 se no final das contas eu vou somar tudo. Logo, eu posso ignorar o fato
de que eu preciso transformar Compra em number. Posso seguir tranquilamente
de Compra =[]> Produto => valor. E então somar tudo em uma bolada só:
// original
compras.compras
.map(c => c.flatMap(c => c.produtos)
.map(p => p.qtd * p.valor_unitario)
.reduce((acc, curr) => acc + curr, 0))
.reduce((acc, curr) => acc + curr, 0)
// usando as propriedades da soma
compras.compras // Array<Compra>
.flatMap(c => c.produtos) // Array<Produto>
.map(p => p.qtd * p.valor_unitario) // Array<number>
.reduce((acc, curr) => acc + curr, 0) // number
E aqui temos a resposta ao desafio, usando uma única redução de soma:
compras.compras
.flatMap(c => c.produtos)
.map(p => p.qtd * p.valor_unitario)
.reduce((acc, curr) => acc + curr, 0)
Pequenas variações
Se eu quisesse apenas as compras que foram realizadas em maio de 2022? Bem, aqui posso aplicar um filtro nas compras:
compras.compras
.filter(comprasMaio2022)
.flatMap(c => c.produtos)
.map(p => p.qtd * p.valor_unitario)
.reduce((acc, curr) => acc + curr, 0)
e implementar a função de filtro comprasMaio2022 de modo adequado.
E se forem apenas para os produtos de código "a", "b" e "c"?
Novamente, apenas um filtro nos produtos antes de transformar eles em
valores e reduzir:
compras.compras
.flatMap(c => c.produtos)
.filter(produtosAdequados)
.map(p => p.qtd * p.valor_unitario)
.reduce((acc, curr) => acc + curr, 0)
e implementar a função de filtro produtosAdequados de modo adequado.
