Desenvolvimento

11 mar, 2019

Flat/FlatMap do JavaScript na prática

Publicidade

Neste artigo vamos ver como 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, o que significa que estarão na próxima 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 de Michael Ficarra, Brian Terlson e Mathias Bynens. Mas qual a importância deles e como podemos utilizá-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.

Essa operação de array pode nos ajudar a 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 operações de arrays.

Estamos familiarizados com funções como o map, filter e reduce, que são responsáveis por transformar os elementos dos 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 como objetivo 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 abaixo:

[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 faz a função ficar plana em dois 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 alguns destes arrays e utilizar a operação push para inserir os valores de um array dentro do outro.

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

Outro método comum, caso queira criar um novo array, é 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 flat traz consigo uma maneira interessante, sem a necessidade de spreads, para concatenar os 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 própria criação array, colocando um ternário no proprio array. Caso a condição seja verdadeira, insira o valor ‘a’ – caso contrário, insira 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 filtrar esses valores de alguma forma.

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

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

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

Criando uma cópia de um array

Quando queremos criar uma cópia de um array mudando sua referência.

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 a partir de uma função f, retorna um novo array com cada um desses valores transformados. A função f que recebe um elemento input e torna um elemento output.

Com o flatMap cada elemento é iterado e, a partir de uma função f, retorna um array de valores. A função f que recebe um elemento input, 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 à 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()

Veremos alguns usos práticos do FlatMap.

Filtrar e transformar arrays

Exemplo 1

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

Supondo um array de números de 1 a 10.

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

Queremos transformar este array em apenas números antecessores de números primos.

Supondo que eu tenho uma função isPrime que retorna verdadeiro ou falso caso o número seja 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 teríamos que novamente iterar pelos itens para retornar um novo array com cada valor subtraído 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 subtraído 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 esse item deve ou não ser listado. Se sim, devo dar fetch nessa propriedade.

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

Sem o flatMap, uma solução viável seria utilizar o filter para filtrar, caso a propriedade toList fosse 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 esse problema criando uma função em que caso o toList seja verdadeiro, ele retorna um array com o fetch do id. Caso contrário, ele 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 específicos desses 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 eu 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 método 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, seus elementos são concatenados e podemos adicionar elementos condicionai. 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á contam com o suporte dos principais navegadores (Chrome 69, Firefox 62, Opera 56, Safari 12 e Android WebView 69) e na versão 11.0.0 do NodeJs.

É possível, também, importar proposals pelo Babel 7. O FlatMap já está em stage 4, por isso, é 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.