Se você já trabalhou com arrays, Optional, Promises ou Streams, você já usou flatMap — ou já sentiu na pele o problema que ele resolve, mesmo sem saber o nome dele. Neste post eu vou contar a história por trás dessa função, mostrar por que ela existe e como ela aparece disfarçada em praticamente toda linguagem moderna.
O vilão da história: map
Todo mundo ama map. Ele pega uma lista, aplica uma função em cada item, e devolve outra lista do mesmo tamanho:
[1, 2, 3].map(x => x * 2) // [2, 4, 6]
Simples, previsível, elegante. O problema começa quando a função que você passa para map não devolve um valor, mas devolve outra coleção.
Imagine que você tem uma lista de frases e quer a lista de todas as palavras:
const frases = ["bom dia", "como vai você"];
frases.map(frase => frase.split(" "))
// [["bom", "dia"], ["como", "vai", "você"]]
Você queria uma lista plana de palavras. O que você recebeu foi uma lista de listas. map fez exatamente o que prometeu — só que o resultado ficou "embrulhado" um nível a mais do que você esperava.
A solução ingênua (e por que ela não é suficiente)
A saída óbvia é fazer map e depois achatar manualmente:
const listaDeListas = frases.map(frase => frase.split(" "));
const achatada = [].concat(...listaDeListas);
// ["bom", "dia", "como", "vai", "você"]
Funciona. Mas repare que esse padrão — mapear e depois achatar — não é um caso isolado do seu código. Ele se repete o tempo todo: em arrays, em Optional, em Promises, em Streams, em Observables. Sempre que uma transformação pode gerar "zero, um ou vários resultados", ou "um resultado que já vem embrulhado", você cai nesse mesmo padrão.
Quando um padrão se repete tanto assim, em tantos contextos diferentes, a comunidade de programação funcional faz o que sempre faz: dá um nome pra ele e transforma numa operação de primeira classe. Nasce o flatMap.
frases.flatMap(frase => frase.split(" "))
// ["bom", "dia", "como", "vai", "você"]
Mesma ideia, uma linha, sem estrutura intermediária.
"Mas eu já podia fazer map + flat, por que criar uma função nova?"
Pergunta justa. Três respostas:
1. Performance. map seguido de flat cria uma lista de listas no meio do caminho, só para jogá-la fora logo em seguida. flatMap achata durante a própria iteração — sem esse lixo intermediário.
2. A ideia é maior que arrays. "Transformar e achatar um nível" não é um truque específico de listas. É algo que faz sentido para qualquer tipo de "caixa" que pode conter outra caixa do mesmo tipo: Promise<Promise<T>>, Optional<Optional<T>>, Stream<Stream<T>>. Por isso vale a pena ter um nome e um contrato universais.
3. Composição. flatMap é o que te permite encadear operações sem acumular aninhamento a cada passo — e é exatamente esse encadeamento que sustenta código assíncrono legível, tratamento de erros sem pilha de if, e pipelines de dados limpos.
O nome bonito por trás disso: mônada
Aqui vai o motivo "acadêmico". Em programação funcional existe uma abstração chamada mônada, e toda mônada precisa oferecer uma operação chamada bind (em Haskell, o operador >>=). Essa operação é, na prática, o flatMap.
A assinatura de tipo resume tudo:
map: Container<A> + (A -> B) -> Container<B>
flatMap: Container<A> + (A -> Container<B>) -> Container<B>
Ou seja: se sua função de transformação já devolve o mesmo tipo de "caixa" em que você está trabalhando, você não quer map (que geraria Container<Container<B>>), você quer flatMap.
Guarde essa assinatura — ela é a mesma em qualquer linguagem, qualquer contexto.
flatMap disfarçado no seu dia a dia
Em arrays
const pedidos = [
{ cliente: "Ana", itens: ["arroz", "feijão"] },
{ cliente: "Bruno", itens: ["leite"] },
];
pedidos.flatMap(pedido => pedido.itens);
// ["arroz", "feijão", "leite"]
Bônus: filtrar e mapear em uma passada só, retornando array vazio pra descartar:
[1, 2, 3, 4, 5].flatMap(n => n % 2 === 0 ? [n] : []);
// [2, 4]
Em Optional (Java), pra evitar cascata de if (x != null)
Optional<Endereco> endereco = buscarUsuario(id)
.flatMap(usuario -> buscarPerfil(usuario))
.flatMap(perfil -> buscarEndereco(perfil));
Se qualquer passo vier vazio, a cadeia inteira vira vazia — sem checagem manual em cada etapa.
Em Promises — sim, .then é um flatMap disfarçado
buscarUsuario(1).then(usuario => buscarPedidos(usuario));
// Promise<Pedidos>, não Promise<Promise<Pedidos>>
Se .then não achatasse automaticamente, cada passo assíncrono adicionaria mais uma camada de Promise dentro de Promise. async/await é, por baixo dos panos, açúcar sintático em cima dessa mesma ideia.
Em Streams e Observables
List<String> palavras = frases.stream()
.flatMap(frase -> Arrays.stream(frase.split(" ")))
.collect(Collectors.toList());
clique$.pipe(
flatMap(evento => fazerRequisicaoHttp(evento))
)
Quando usar map, quando usar flatMap
| Situação | Use |
|---|---|
| A função transforma 1 elemento em 1 elemento | map |
| A função pode devolver 0, 1 ou vários elementos | flatMap |
Você tem Container<Container<T>> e quer Container<T> |
flatten (ou flatMap(x => x)) |
| Você quer encadear operações que podem falhar/estar vazias | flatMap |
| Você quer encadear operações assíncronas | flatMap (.then, async/await) |
Regra de bolso: se dentro do seu map você está retornando um array, um Optional ou uma Promise, é sinal de que você deveria estar usando flatMap.
Bônus: implementando do zero
Nada fixa mais um conceito do que implementá-lo. Aqui vai flatMap para arrays, sem nenhuma mágica:
function flatMap(array, fn) {
const resultado = [];
for (const item of array) {
const transformado = fn(item); // uma coleção
for (const sub of transformado) {
resultado.push(sub);
}
}
return resultado;
}
flatMap([1, 2, 3], x => [x, x * 10]);
// [1, 10, 2, 20, 3, 30]
É literalmente map (aplicar fn) mais achatar um nível (o segundo for), numa função só.
Cuidado com essas pegadinhas
flatMapachata um nível só, não recursivamente.[[[1]]].flatMap(x => x)vira[[1]], não[1].- Se a sua função sempre devolve um valor simples, use
map—flatMapali só adiciona ruído. - A função passada para
flatMapprecisa devolver uma coleção/container, não um valor cru.
Pra fechar
flatMap não é frescura de programação funcional — é a resposta natural para um problema que qualquer desenvolvedor esbarra cedo ou tarde: transformar dados que já vêm "embrulhados" sem empilhar embrulhos. Da próxima vez que você se pegar fazendo map e depois achatando na mão, ou empilhando if (x != null) pra tratar valores opcionais, lembre: provavelmente existe um flatMap esperando por você.