DEV Community

Cover image for Flat/FlatMap do Javascript na Prática
/ana.(js|clj)/g
/ana.(js|clj)/g

Posted on

Flat/FlatMap do Javascript na Prática

Aprendendo a utilizar as novas operações de Array do Javascript

A proposta

Foram confirmadas recentemente as novas propostas de funcionalidades que chegaram no estágio 4 do ECMAScript, significando que estarão na proxima especificação oficial e terão sua implementação na linguagem!!

Dentre elas temos dois novos bebês ❤ chamados Array.prototype.flat() e o Array.prototype.flatMap(), baseados na proposta do Michael Ficarra, Brian Terlson, Mathias Bynens. Mas qual a importancia deles e como podemos utiliza-los?

O flatMap (Muitas vezes chamado de concatMap, fmap ou >>= em outras linguagens) é um pattern comum que vem da programação funcional de linguagens como Scala ou Haskell. Esta operação de array pode nos ajudar solucionar problemas em que temos de iterar por exemplo, arrays com itens complexos. Você pode já ter ouvido falar de implementações similares de Flatmap por exemplo no RxJS para lidar com Observables.

Mas diferente de outras linguagens, que utilizam o flatMap como uma operação para manipular objetos, strings , tendo usos até mesmo como meio de lidar com valores opcionais e monadas. A sua implementação no Javascript se limita a apenas operações de arrays.

Estamos familiares com funções como o map , filter e reduce que são responsáveis por transformar os elementos do arrays em novos valores a partir de uma função.

// Adicionando +1 para todos os elementos do array
[1, 2, 3].map(x => x + 1); // [2, 3, 4]

// Filtrando elementos impares do array
[1, 2, 3].filter(x => x % 2 == 1) // [1 , 3]

// Somando os elementos do array
[1, 2, 3].reduce((acc, x) => acc + x) // 6

Similarmente, o flatMap recebe uma função como argumento e funde os conceitos de flat com o já conhecido map .

Mas o que é o flat?

Array.prototype.flat()

O Array.prototype.flat(), também conhecido como flatten, tem o intuito de deixar nosso array plano recursivamente em uma profundidade especificada como argumento, ou seja, é uma operação que concatena os elementos de um array.

Por padrão a função de flat planifica em um nível(.flat(1)) como no exemplo:

[1, 2, [3, 4]].flat();
// [1, 2, 3, 4]

[1, 2, [3, 4, [5, 6]]].flat();
// [1, 2, 3, 4, [5, 6]]

Passando o número 2 como argumento a função fica plana em 2 níveis.

[1, 2, [3, 4, [5, 6]]].flat(2);
// [1, 2, 3, 4, 5, 6]

Alguns usos praticos do Flat

Concatenando arrays

Supondo dois arrays contendo alguns números que devem ser concatenados em apenas um array.

const array1 = [1, 2, 3]
const array2 = [4, 5, 6]

Uma forma de fazer isso seria mutar algum destes arrays e utilizar a operação push para inserir os valores do outro array dentro do outro.

array1.push(...array2)
array1 // [1, 2, 3, 4, 5, 6]_

Outro método comum caso eu queria criar um novo array seria utilizar o spread dos arrays dentro de um novo array concatenando seus elementos.

const array3 = [
 array1,
 array2
] // [1, 2, 3, 4, 5, 6]_

A operação de flat nos introduz uma maneira interessante sem a necessidade de spreads para concatenar o elementos deste array.

[array1, array2].flat()

Condicionalmente inserindo valores em um array.

Supondo que caso uma condição seja verdadeira, devo inserir um valor dentro de um array.

Uma forma mais sucinta seria ao invés de um “if”, considerar essa condicional na propria criação array, colocando um ternário no proprio array. Caso a condição é verdadeira, insere o valor ‘a’, caso contrário insere null.

const cond = false;
const arr = [
  'b',
  (cond ? 'a' : null),
]; // ['b', null]

Em condições positivas teremos o elemento esperado ‘a’, mas caso contrário teremos um array sujo com valores “null” e para isso seria necessário de alguma forma filtrar esses valores.

arr.filter(_ => _ !== null) // ['b']

Com o flat podemos fazer de forma simples a inserção de valores caso a condição é verdadeira com uma condicional (cond ? ['a'] : []). Pois já que o proprio flat concatena arrays, a concatenação de um array vazio caso uma condição falsa não geraria a insersão de valores desnecessários.

const cond = false;
const arr = [
  (cond ? ['a'] : []),
  'b',
].flat(); // ['b']

Criando uma copia de um array

Quando queremos criar uma copia de um arr mudando sua referencia.

const x = [1, 2, 3, [4]]

const y = x.flat(0)
y[0] = 3

x // [1,2,3,[4]]
y // [3,2,3,[4]]

Note que isso vai apenas retornar uma “shallow copy”. Ou seja, objetos dentro do array não serão clonados.

Array.prototype.flatMap()

O flatMap é basicamente um map com flat. Como assim?

Com o map , cada elemento do array é iterado e apartir de uma funçãof retorna um novo array com cada um desses valores transformados. A função fque recebe um elemento input e torna um elemento output.

Com o flatMap , cada elemento é iterado e apartir de uma função f retorna um array de valores. A função fque recebe um elemento input e e cada elemento pode ser transformado em nenhum ou mais elementos de output.

Relação entre Map(One to one) e FlatMap(One to many)

Ambos flatMap e map recebem uma função f como argumento que gera um novo array de retorno com base nos items do array de origem.

Sequencialmente o flatMap seria similar a aplicação de uma função dentro de um map seguido de uma operação de flat planificando o Array.

[1, 2, 3]
 .map(item => [item, item \* 100]); //[[1, 100], [2, 200], [3, 300]]
 .flat() // [1, 100, 2, 200, 3, 300]

[1, 2, 3].flatMap(item => [item, item * 100]);
// [1, 100, 2, 200, 3, 300]

// Mesma operação :)

Similarmente, usando o flatMap com uma função identidade(x => x), em que inutilizamos o seu map , temos exatamente o que seria apenas um flat.

As seguintes operações são equivalentes:

arr.flatMap(x => x)
arr.map(x => x).flat()
arr.flat()

Alguns usos praticos do FlatMap

Filtrar e transformar arrays

Exemplo 1

Podemos utilizar a operação de flapMap() como meio de filtrar elementos em arrays e transforma-los.

Supondo uma array de numeros de 1 a 10.

const x = [1, 2 ,3 ,4, 5, 6, 7, 8, 9, 10]

Queremos transformar este array em apenas numeros antecessores de numeros primos. Supondo que eu tenho uma função isPrime que me retorna verdadeiro ou falso caso o numéro é um primo. Podemos primeiramente utilizar a função filter para filtrar os valores em apenas primos_._

x.filter(i => isPrime(i)) // [2, 3, 5, 7]

Porém para listar os antecessores do array teriamos de novamente iterar pelos itens para retornar um novo array com cada valor subtraido por 1.

x.filter(i => isPrime(i))
 .map(i => i - 1) // [1, 2, 4, 6]

Com o flatMap podemos fazer ambas as operações em apenas uma iteração de array em que com uma operação ternária retornamos ou um array com o valor subtraido por 1 ou uma array vazio.

x.flatMap(i => isPrime(i) ? [i  1] : []) // [1, 2, 4, 6]

Sendo assim: é um map , que iteraria os 10 elementos do array e geraria 10 arrays, seguido de um flat planificando em apenas um array:

x.map(i => isPrime(i) ? [i  1] : []) // [[],[1],[2],[],[4],[],[6]..] 
 .flat() // [1, 2, 4, 6]

Exemplo 2

Tenho um array de ids de objetos e uma propriedade booleana indicando se este item deve ou não ser listado, se sim devo dar fetch nesta propriedade.

const items = [
 { id : 1, toList : true}
 { id : 2, toList : false},
]

Sem o flatMap uma solução viavel seria utilizar o filter para filtrar caso a propriedade toList é verdadeira e em seguida seria necessário utilizar um map para efetivamente dar fetch nesses ids.

items
 .filter(i => i.toList)
 .map(i => fetch(i.id)) // [Promise]

Com apenas um flatMap podemos resolver este problema criando uma função em que caso o toList é verdadeiro ele retorna um array com o fetch do id, caso contrário retorna um array vazio que será concatenado.

Promise.all(items.flatMap(i => i.toList
 ? [fetch(i.id)]
 : [])) // [...]

Exemplo 3

Podemos usar para extrairmos apenas um tipo de dado de um objeto em tratativas. Por exemplo, em um array de objetos cuja tratativa de erros de um try / catch retorna apenas os valores dos resultados ou apenas os erros.

const results = arr.map(x => {
try {
  return { value: fazerAlgo(x) };
    } catch (e) {
return { error: e };
    }
});

flatMap pode ser nosso aliado para podermos extrair apenas os erros ou apenas os valores especificos destes resultados por meio de uma operação ternária:

const values = results. flatMap(
    result => result.value ? [result.value] : []);

const errors = results.flatMap(
    result => result.error ? [result.error] : []);

Pegando elementos de um array de objetos com arrays aninhados.

Supondo que tenho um array de objetos de cestas de frutas em que dentro do objetos listamos em “itens” as frutas dentro da cesta.

const cestas = [
 { id : 1, itens : [Maça, Banana]},
 { id : 2, itens : [Banana, Abacaxi]}
]

Se quero listar todas as frutas dentro de cestas no map seria necessário iterar pelo array e pegar a propriedade “itens” de cada objeto.

cestas.map(x => x.itens) // [Array(2), Array(2)]

Apenas com o map teriamos arrays de arrays.

cestas.flatMap(x => x.itens) // [“Maça”, “Banana”, “Banana”, “Abacaxi”]

Com o flatMap já temos a concatenação dos elementos do array e conseguimos obter todos os elementos listados dentro de objetos.

Indexando em lista

Supondo uma lista de compras, para listá-las entre vírgulas em um componente “GroceryList” podemos utilizar o flatMap. A função cujo metodo recebe pode ter um segundo argumento com o index do array assim como o map ou filter. Por exemplo:

['Foo','Bar'].map((x, index) => `${index}${x}`); _// ['0Foo', '1Bar']_

Quando retornamos um array desta função, os seus elementos são concatenados e podemos adicionar elementos condicionais (como por exemplo, a vírgula após o primeiro elemento da lista).

class GroceryList extends React.Component {
render () {
const {groceries, handleClick} = this.props;
return groceries. flatMap (
            (_food_, _index_) => [
                ...(_index_ === 0 ? [] : [', ']),
                <a key={_index_} href=""
                   onClick={e => handleClick(_food_, e)}>
                   {_food_}
                </a>,
            ]);
    }
}

Suporte de Navegadores

O flat e o flatMap já estão com suporte nos principais navegadores(Chrome 69, Firefox 62, Opera 56, Safari 12, Android WebView 69) e na versão 11.0.0 do NodeJs ❤️🧡💛💚💙💜.

É possivel tambem importar proposals pelo Babel 7. Pelo FlatMap já estar em stage 4 épreciso importar especificamente a funcionalidade.

Conclusão

Cada vez mais vemos mudanças para agradar todas as formas / paradigmas do Javascript. Desde 2015 vemos a linguagem suportando outros estilos de orientação a objetos, e agora vemos a adição de elementos comuns de linguagens funcionais como FlatMap e quem sabe futuramente o Pipeline Operator, Pattern Matching e Partial Application 🤞.

Espero que você tenha gostado do artigo ❤

Quem gostou bate palminha pra que o artigo alcançe outros amiguinhos.

👏 👏 👏 👏 👏 👏 👏 👏 👏

Oldest comments (0)